Skip to content

js_parser: name an anonymous decorated class assigned with ??=, ||= or &&= after the target - #38924

Open
robobun wants to merge 3 commits into
mainfrom
farm/4055ec3e/decorated-class-logical-assign-name
Open

robobun wants to merge 3 commits into
mainfrom
farm/4055ec3e/decorated-class-logical-assign-name

Conversation

@robobun

@robobun robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • With standard decorators, a ??= @dec class {} (also ||=, &&=) names the class "" and passes "" to the decorator as context.name. a = @dec class {} gives "a", as node does for all four operators without the decorator.
  • The cause is visit_right_and_finish (src/js_parser/visit/visit_binary.rs:124). It records the assignment target as the class name only for =. Found by inspection, there is no user report.

Fix

  • Record the name for BinNullishCoalescingAssign, BinLogicalOrAssign and BinLogicalAndAssign too, under the same conditions: an anonymous class that will be lowered, and an identifier target. The emitted code changes, so EXPECTED_VERSION in src/jsc/RuntimeTranspilerCache.rs goes from 32 to 33.
  • Correct because the specification gives these operators the same NamedEvaluation rule as =. += and member targets (o.p ??= class {}) have no such rule and stay excluded. esbuild 0.25.0 and tsc 6.0.2 give the same names.
  • Verified: test/bundler/transpiler/es-decorators.test.ts, block anonymous decorated class assigned with a logical assignment operator. 2 of its 3 tests fail on main without the change. Other suites: see Notes.
  • Self-reviewed: 3 concerns raised, 3 addressed (see Notes).

Background

  • NamedEvaluation names an anonymous function or class after its position (const a = class {}, a ??= class {}). The expression must be directly in that position.
  • The lowering (js_parser: lower standard decorators without moving class members #40833) keeps a class expression without a class decorator in place, so the engine names it. With a class decorator it wraps the class in a comma expression, which the engine cannot name.
  • Visitor sites that give a name store it in p.decorator_class_name. The lowering passes it to __decorateElement as a string, for .name and context.name. No class binding exists, so the class body still sees the assigned variable.
Notes

Repro

const seen = [];
function dec(cls, ctx) { seen.push(ctx.name); }
let a; a ??= @dec class {};
let b = null; b ||= @dec class {};
let c = 1; c &&= @dec class {};
let d; d = @dec class {};
console.log(JSON.stringify([seen, [a.name, b.name, c.name, d.name]]));
build output
node 26.3.0, decorators removed, second array ["a","b","c","d"]
esbuild 0.25.0 and tsc 6.0.2 output, run in node [["a","b","c","d"],["a","b","c","d"]]
main 09bb546, bun run and bun build [["","","","d"],["","","","d"]]
this PR, bun run and bun build [["a","b","c","d"],["a","b","c","d"]]

With h.m ??= @dec class {} and s += @dec class {} added, esbuild, tsc and this PR all give "" for those two.

Emitted code for a ??= @dec class {} (temporary names shortened)

a ??= (_dec = [dec], class {
  static {
    _init = __decoratorStart(undefined);
    _a = __decorateElement(_init, 0, "a", _dec, this);
  }
}, __runInitializers(_init, 1, _a), _a);

Before the change the third argument of __decorateElement is "".

Sites that this PR leaves alone on purpose

Self-review

  • The emitted code changes but the transpiler cache version did not. Addressed: EXPECTED_VERSION is now 33. Other open PRs also take 33, so the number can change at merge time.
  • The description did not say how the problem was found and did not name the =-only keep-names arm. Addressed in Problem and in the list above.
  • Two of the four tests passed with and without the change. Addressed: the test for classes with only member decorators is gone. The test for the excluded cases stays, because it pins the operators and targets that the Fix bullet excludes.

History

Tests in the new block

  • class decorator sees the target name as context.name: the three operators, =, a chained Outer = Inner ??= @dec class {} (named Inner) and an assignment inside a function. It checks context.name and .name. Fails on main without the change.
  • the class body still refers to the variable being assigned: a static initializer reads the value that the variable had during definition (null under ||=, 1 under &&=), a method sees a later reassignment, the body of a class that a decorator replaced sees the replacement, and a method can assign to the variable. Fails on main without the change (the names are "").
  • the target name is only used for an anonymous class directly on the right: an explicit class name, a member target, a class nested in the right operand, and +=. Passes before and after.
  • The expected .name values and the scoping results are what node prints for the same programs with the decorators removed.

Suites run on the debug build of this branch

es-decorators.test.ts (421), es-decorators-esbuild.test.ts (147), decorators.test.ts (24), decorator-metadata.test.ts (5), transpiler.test.js (214), test/cli/run/transpiler-cache.test.ts and test/regression/issue/30887.test.ts (25). cargo fmt --check is clean for bun_js_parser and bun_jsc.


[human-review] gate passed · iteration 2 · 3 files touched

fails on main (without fix)
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/bundler/transpiler/es-decorators.test.ts
bun test v1.4.3 (b99371011)

test/bundler/transpiler/es-decorators.test.ts:
(pass) ES Decorators > class decorators > basic class decorator [425.40ms]
(pass) ES Decorators > class decorators > class decorator receives correct context [331.74ms]
(pass) ES Decorators > class decorators > class decorator can replace class [524.85ms]
(pass) ES Decorators > class decorators > multiple class decorators apply in reverse order [304.50ms]
(pass) ES Decorators > method decorators > instance method decorator [298.94ms]
(pass) ES Decorators > method decorators > static method decorator [345.18ms]
(pass) ES Decorators > method decorators > method decorator context has correct access [350.67ms]
(pass) ES Decorators > getter decorators > getter decorator [383.70ms]
(pass) ES Decorators > setter decorators > setter decorator [303.73ms]
(pass) ES Decorators > field decorators > field decorator receives undefined value [308.04ms]
(pass) ES Decorators > field decorators > multiple field decorators [387.63ms]
(
... (truncated)

release without fix: 354 FAILED
bun test v1.4.3-canary.1 (b99371011)

test/bundler/transpiler/es-decorators.test.ts:
(pass) ES Decorators > class decorators > basic class decorator [7.99ms]
(pass) ES Decorators > class decorators > class decorator receives correct context [7.76ms]
(pass) ES Decorators > class decorators > class decorator can replace class [7.18ms]
(pass) ES Decorators > class decorators > multiple class decorators apply in reverse order [7.02ms]
(pass) ES Decorators > method decorators > instance method decorator [6.94ms]
(pass) ES Decorators > method decorators > static method decorator [8.06ms]
(pass) ES Decorators > method decorators > method decorator context has correct access [6.74ms]
(pass) ES Decorators > getter decorators > getter decorator [6.89ms]
(pass) ES Decorators > setter decorators > setter decorator [6.05ms]
(pass) ES Decorators > field decorators > field decorator receives undefined value [5.84ms]
(pass) ES Decorators > field decorators > multiple field decorators [6.13ms]
(pass) ES Decorators > field decorators > static field decorator [5.92ms]
(pass) ES Decorators > non-ASCII string-literal keys > Bun.Transpiler output preserves the key [0.64ms]
(pass) ES Deco
... (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/pr_gate.xml" test/bundler/transpiler/es-decorators.test.ts
bun test v1.4.3 (b99371011)

test/bundler/transpiler/es-decorators.test.ts:
(pass) ES Decorators > class decorators > basic class decorator [308.09ms]
(pass) ES Decorators > class decorators > class decorator receives correct context [409.17ms]
(pass) ES Decorators > class decorators > class decorator can replace class [449.51ms]
(pass) ES Decorators > class decorators > multiple class decorators apply in reverse order [317.11ms]
(pass) ES Decorators > method decorators > instance method decorator [343.09ms]
(pass) ES Decorators > method decorators > static method decorator [387.61ms]
(pass) ES Decorators > method decorators > method decorator context has correct access [388.07ms]
(pass) ES Decorators > getter decorators > getter decorator [420.74ms]
(pass) ES Decorators > setter decorators > setter decorator [347.06ms]
(pass) ES Decorators > field decorators > field decorator receives undefined value [392.05ms]
(pass) ES Decorators > field decorators > multiple field decorators [390.63ms]
(
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 652ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/23] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 243 extern-C blocks audited
[2/23] gen cpp.rs (cppbind)
[3/23] gen JS modules (bundle-modules)
Preprocess modules (9189ms)
Bundle modules (50ms)
Postprocesss modules (142ms)
Bundle Functions (612ms)
Generate Code (41ms)

[10.04s] Bundled "src/js" for production
  2600 kb
  197 internal modules
  13 native modules
  50 internal functions across 16 files
[3/7] cargo bun_runtime → libbun_runtime.a
�[1m�[92m   Compiling�[0m bun_threading v0.0.0 (/workspace/bun/src/threading)
�[1m�[92m   Compiling�[0m bun_io v0.0.0 (/workspace/bun/src/io)
�[1m�[92m   Compiling�[0m bun_watcher v0.0.0 (/workspace/bun/src/watcher)
�[1m�[92m   Compiling�[0m bun_crash_handler v0.0.0 (/workspace/bun/src/crash_handler)
�[1m�[92m   Compiling�[0m bun_event_loop v0.0.0 (/workspace/bun/src/event_loop)
�[1m�[92m   Compiling�[0m bun_sourcemap v0.0.0 (/workspace/bun/src/sourcemap)
�[1m�[92m   Compiling�[0m bun_css v0.0.0 (/wo
... (truncated)
diff hotspot
src/js_parser/visit/visit_binary.rs           | 12 +++-
 src/jsc/RuntimeTranspilerCache.rs             |  3 +-
 test/bundler/transpiler/es-decorators.test.ts | 97 +++++++++++++++++++++++++++
 3 files changed, 109 insertions(+), 3 deletions(-)

gate history · 3 passed · 1 rejected · iteration 2

evidence per changed file
file                                           reads  edits  tests
src/js_parser/visit/visit_binary.rs                0      0      3
src/jsc/RuntimeTranspilerCache.rs                  0      0      2
test/bundler/transpiler/es-decorators.test.ts      0      0      2

root cause · written by the author bot

The parser only set the decorator class name for anonymous class expressions on the right side of a plain = assignment, so classes assigned via ??=, ||=, or &&= lost the inferred name when standard decorators were lowered. The fix widens the condition in visit_right_and_finish to cover those three logical assignment operators, matching the set for which the spec performs NamedEvaluation on the right operand, while leaving compound arithmetic operators and member-expression targets excluded. Because this changes generated output, the runtime transpiler cache version was bumped from…

@coderabbitai

coderabbitai Bot commented Aug 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Essentials

Run ID: 28294fe3-60f8-4c84-b4b9-b2232a9e670e

📥 Commits

Reviewing files that changed from the base of the PR and between 8ce1b32 and 92dcfe7.

📒 Files selected for processing (2)
  • src/jsc/RuntimeTranspilerCache.rs
  • test/bundler/transpiler/es-decorators.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


Walkthrough

The parser now propagates decorator names for anonymous named class expressions used with =, ??=, ||=, and &&=. Tests cover inferred names, decorator context names, class binding behavior, and cache versioning.

Changes

Decorator name propagation

Layer / File(s) Summary
NamedEvaluation assignments and validation
src/js_parser/visit/visit_binary.rs, test/bundler/transpiler/es-decorators.test.ts
Anonymous named class expressions now propagate decorator names for four assignment operators. Tests cover chained assignments, nested expressions, member targets, function scopes, explicit names, and class binding behavior.
Runtime transpiler cache versioning
src/jsc/RuntimeTranspilerCache.rs
The runtime transpiler cache format version increases from 32 to 33 for the expanded decorator-name behavior.

Suggested reviewers: jarred-sumner

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 92dcf

The logical-assignment decorator naming change is covered for its intended operators and exclusions, with no actionable merge risk identified.

🚥 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 parser change for anonymous decorated classes assigned with ??=, ||=, and &&=. It is concise and directly matches the main changeset.
Description check ✅ Passed The description explains the problem, fix, scope, specification rationale, tests, verification results, and cache version change. It does not use the exact template headings What does this PR do? an…

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

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:48 PM PT - Sep 13th, 2026

✅ @robobun, your commit 92dcfe74af2739995025def4e17f56a6a7bbbbd6 passed in Build #115342! 🎉


🧪   To try this PR locally:

bunx bun-pr 38924

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

bun-38924 --bun

@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 — extends the existing decorator_class_name propagation from = to the three logical assignment operators, matching the spec's NamedEvaluation rule.

What was reviewed:

  • Confirmed ??=/||=/&&= perform NamedEvaluation per ECMA-262 §13.15.2 while arithmetic compound assigns do not — the operator set is exactly right.
  • The save/restore of prev_decorator_class_name at lines 121/182 already brackets all right-operand visits, so the new arms are covered without further changes.
  • Checked the sibling decorator_class_name sites in visit/mod.rs, visit_stmt.rs and visit_expr.rs — they cover declarators, destructuring defaults, property values and export default, none of which can carry a logical operator, so this is the only site that needed changing.
  • Tests cover all three operators (method + class decorator paths), the chained Outer = Inner ??= … case, and negative guards for named classes, member targets, nested classes, and +=.
Extended reasoning...

Overview

The PR extends one condition in src/js_parser/visit/visit_binary.rs: the block that records the assignment target's identifier name into p.decorator_class_name before visiting the right operand now fires for BinNullishCoalescingAssign, BinLogicalOrAssign and BinLogicalAndAssign in addition to BinAssign. Three new test.concurrent blocks are added to test/bundler/transpiler/es-decorators.test.ts.

Security risks

None. This is compile-time transpiler metadata (the .name given to a lowered anonymous class expression and the context.name seen by a class decorator). There is no user-input parsing, allocation sizing, or FFI change.

Level of scrutiny

Low-to-medium. The Rust change is a mechanical widening of an equality check into a four-arm matches!. Everything around it — the was_anonymous_named_expr gate, the EClass + should_lower_standard_decorators check, the EIdentifier left-side check, and the prev_decorator_class_name save/restore around the visit — is unchanged and already exercised by the existing BinAssign path. I verified that the restore at line 182 runs after every branch of the operator-dispatch match, so setting the name for the three new operators cannot leak into unrelated visits.

The spec claim checks out: ECMA-262 §13.15.2 defines the runtime semantics for &&=, ||= and ??= to call NamedEvaluation of the RHS with the LHS reference name when the RHS is an anonymous function/class definition, exactly as = does; the arithmetic/bitwise compound assignments do not. So the four-operator set is complete and does not over-apply.

Other factors

  • The chained-assignment test (Outer = Inner ??= class {…} → name Inner) confirms the inner-most NamedEvaluation position wins, which is what the save/restore stacking already produces.
  • Negative guards prove the change doesn't over-apply: an explicit class name still wins (is_anonymous_named is false), a member-expression target is still excluded (EIdentifier gate), a class not directly on the RHS is still excluded (EClass gate), and += is still excluded (operator gate).
  • I checked the other decorator_class_name write sites (visit/mod.rs for let x = … and destructuring/parameter defaults, visit_stmt.rs for export default, visit_expr.rs for object property values and array-pattern defaults). None of those grammatical positions accept a logical assignment operator, so visit_binary.rs is indeed the only site that needed this change, as the PR description states.
  • Tests use test.concurrent per repo convention for independent subprocess-spawning tests, assert stderr before exitCode, and use exact toEqual on structured JSON output.

Comment thread src/js_parser/visit/visit_binary.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 — extends the existing decorator_class_name propagation from = to ??=/||=/&&=, which are exactly the assignment operators the spec defines to perform NamedEvaluation.

What was reviewed:

  • Confirmed the four-operator set matches the ECMAScript NamedEvaluation rule; compound arithmetic/bitwise assigns are correctly excluded.
  • The save/restore of prev_decorator_class_name was already in place, so the chained Outer = Inner ??= class {} case and nested visits behave the same as for =.
  • New tests cover all three operators (method + class decorator paths), plus negative guards for explicit names, member targets, nested classes, and +=.
  • The comment-cop bot flag on line 124 looks like a false positive — the added line is a one-sentence spec citation, not a workaround justification.
Extended reasoning...

Overview

This PR widens a single condition in src/js_parser/visit/visit_binary.rs:124 so that p.decorator_class_name is recorded not only for BinAssign but also for BinNullishCoalescingAssign, BinLogicalOrAssign, and BinLogicalAndAssign. The rest of the block (anonymous-class check, should_lower_standard_decorators gate, identifier-only LHS, save/restore of the previous value) is unchanged. Three new test.concurrent blocks are added to test/bundler/transpiler/es-decorators.test.ts.

Security risks

None. This is a transpiler naming fix affecting the .name property and context.name of lowered decorated class expressions. No untrusted input parsing, no memory management, no FFI.

Level of scrutiny

Low-to-medium. The change is a mechanical widening of an existing, already-correct pattern to three sibling operators. I verified against the ECMAScript spec that exactly =, &&=, ||=, and ??= perform NamedEvaluation of an anonymous RHS with an identifier LHS — the arithmetic/bitwise compound assignments do not, and the code correctly excludes them. The consumer side in visit_expr.rs (decorator_name_from_context = p.decorator_class_name) reads the value the same way regardless of which operator set it, so no downstream change is needed.

Other factors

  • Test coverage is thorough for a change this size: positive cases for each operator via both the method-decorator path (.name was _class) and the class-decorator path (context.name was ""), plus negative guards (explicitly named class, member-expression target, class nested inside the RHS, +=) that pass before and after — so the change is scoped precisely to the spec-mandated cases.
  • No CODEOWNERS entry covers these files.
  • The github-actions "comment-cop" inline flag on the two-line comment appears to be a false positive: the added second line ("Only these four operators perform NamedEvaluation of the right side") is a concise spec citation explaining a non-obvious operator set, not a paragraph justifying a workaround. Per the repo's own guidance, that's exactly the kind of comment that belongs.
  • No prior human review comments to address; the bug-hunting system found nothing.

@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 — small, spec-aligned extension of the existing = NamedEvaluation path to cover ??=, ||= and &&=.

What was reviewed:

  • Verified the four operators listed are exactly the assignment operators the spec defines NamedEvaluation for; += and other compound ops are correctly excluded.
  • Confirmed the save/restore of prev_decorator_class_name and the EClass/should_lower_standard_decorators/EIdentifier guards are unchanged, so member targets and non-direct RHS classes stay excluded.
  • Checked the new tests cover method-decorator, class-decorator (context.name), chained assignment, accessor-only lowering, and negative guards (explicit name, member target, nested class, +=).
  • The comment-cop bot's feedback was addressed in 659065b (comment is now one line).
Extended reasoning...

Overview

This PR extends the decorator_class_name propagation in visit_right_and_finish (src/js_parser/visit/visit_binary.rs) from BinAssign alone to also include the three logical assignment operators. This makes lowered anonymous decorated classes assigned via ??=/||=/&&= receive the correct .name and class-decorator context.name, matching what the engine would produce natively per the NamedEvaluation rule. Three new test.concurrent blocks in es-decorators.test.ts cover the positive cases and the negative guards.

Security risks

None. This is a transpiler correctness fix that only affects the name string threaded through the standard-decorator lowering. No parsing of untrusted lengths, no allocation changes, no new code paths.

Level of scrutiny

Low-to-medium. The Rust diff is a single condition change — replacing an equality check with a four-arm matches! — inside an existing well-guarded block. The surrounding save/restore of p.decorator_class_name and the inner EClass → should_lower_standard_decorators → EIdentifier gates are untouched, so the new operators inherit exactly the same restrictions as = already had. The spec claim (only = and the three logical assignments perform NamedEvaluation, not arithmetic/bitwise compound assignments) is accurate.

Other factors

  • The github-actions comment-cop flagged a long comment on an earlier commit; commit 659065b shortened it to a single line, which is what the diff now shows.
  • Test coverage is thorough: all three operators for both method and class decorators, the chained Outer = Inner ??= class {} case (correctly names Inner), an accessor-only class (lowered without decorators), and four guard cases confirming explicit names, member targets, nested classes, and += are still excluded. Tests use test.concurrent and the existing runDecorator helper, matching file conventions.
  • The PR description confirms cross-checking against Node's native behavior and passing the full decorator test suite plus clippy/fmt.

@robobun

robobun commented Aug 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: rebased onto main. The former base, #38757, is closed because #40833 superseded it.

Reproduced on a debug build of main (09bb546) with this file:

const seen = [];
function dec(cls, ctx) { seen.push(ctx.name); }
let a; a ??= @dec class {};
let b = null; b ||= @dec class {};
let c = 1; c &&= @dec class {};
let d; d = @dec class {};
console.log(JSON.stringify([seen, [a.name, b.name, c.name, d.name]]));

Main prints [["","","","d"],["","","","d"]]. Node prints the names ["a","b","c","d"] for the same file without the decorators. The output of esbuild 0.25.0 and of tsc 6.0.2 for this file prints [["a","b","c","d"],["a","b","c","d"]] in node. With the change, bun run and bun build print the same.

Tests: test/bundler/transpiler/es-decorators.test.ts, block anonymous decorated class assigned with a logical assignment operator. 2 of its 3 tests fail on main without the visit_binary.rs hunk. All 3 pass with it. The second test is a guard for the excluded cases.

The reason for the stack no longer applies. The lowering on main passes the recorded name to __decorateElement as a string and creates no class binding, so the class body still sees the assigned variable. The third test in the block pins this.

Latest push (92dcfe7): EXPECTED_VERSION of the runtime transpiler cache goes from 32 to 33, because the emitted code changes. The test for classes with only member decorators is gone, because it passes without the change.

@robobun
robobun changed the base branch from main to farm/80aecbe5/decorator-inferred-class-name August 15, 2026 12:58
@robobun
robobun force-pushed the farm/4055ec3e/decorated-class-logical-assign-name branch from 659065b to d33bb32 Compare August 15, 2026 12:58

@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 — extends the existing decorator_class_name propagation from = to the three logical assignment operators, matching the spec's NamedEvaluation rule.

What was reviewed:

  • Confirmed =, ??=, ||=, &&= are exactly the assignment operators that perform NamedEvaluation per ECMA-262; compound arithmetic/bitwise ops are correctly excluded.
  • The save/restore of prev_decorator_class_name around visit_expr is unchanged, so no leakage into unrelated right-hand expressions.
  • Traced decorator_class_name to its consumer in visit_expr.rs (e_class) — the new operators reach it via the same p.visit_expr(&mut e_.right) path as BinAssign already did.
  • Tests cover the positive matrix, context.name for class decorators, negative guards (named class, member target, nested, +=), and that no class binding is introduced.
Extended reasoning...

Overview

The PR widens one condition in src/js_parser/visit/visit_binary.rs: the block that records p.decorator_class_name before visiting the right operand of an assignment now fires for BinNullishCoalescingAssign, BinLogicalOrAssign and BinLogicalAndAssign in addition to BinAssign. Everything else — the was_anonymous_named_expr guard, the EClass+should_lower_standard_decorators guard, the EIdentifier left-hand guard, and the save/restore of the previous value — is byte-identical to before. Four new test.concurrent blocks in test/bundler/transpiler/es-decorators.test.ts exercise the fix and pin the negative cases.

Security risks

None. This is a parser-visitor change that only affects what string is threaded through to the standard-decorator lowering as the class's inferred .name. No untrusted input handling, no memory management, no FFI/JSC surface.

Level of scrutiny

Low-to-medium. The Rust change is a one-condition widening at a site that already handled BinAssign; the mechanism (decorator_class_name → e_class → name_from_context in the lowering) is pre-existing and unchanged. I checked the spec claim: ECMA-262 gives =, &&=, ||= and ??= the NamedEvaluation step for an anonymous function/class RHS with an identifier LHS, and no other assignment operators do, so the operator list is exactly right. The consumer side in visit_expr.rs reads p.decorator_class_name and clears it before visiting the class body, so the existing scoping still holds.

Other factors

  • The comment-cop bot flagged a paragraph-long comment on the first push; the author trimmed it to a single line in 659065b, which is what the current diff shows and which reads fine.
  • No CODEOWNERS cover src/js_parser/ or test/bundler/.
  • Tests follow the file's existing runDecorator harness pattern, use test.concurrent, assert stderr/stdout/exitCode in the recommended order, and include both positive coverage (all three operators, chained assignment, accessor-only lowering, class-decorator context.name) and negative guards (explicit name wins, member target, nested class, +=), plus a check that recording the name does not introduce a class binding that would shadow the outer variable.
  • The author reports the new tests fail on 1.4.0 and pass on the branch, and that the broader decorator/transpiler suites pass; the one red CI lane was an unrelated Windows bake test.

@robobun
robobun force-pushed the farm/80aecbe5/decorator-inferred-class-name branch from 68a8a87 to 1ed1f67 Compare August 15, 2026 13:20
@robobun
robobun force-pushed the farm/4055ec3e/decorated-class-logical-assign-name branch from d33bb32 to b115ed8 Compare August 15, 2026 14:55

@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 — extends the existing decorator_class_name propagation from = to the three logical assignment operators, matching the spec's NamedEvaluation rule.

What was reviewed:

  • Confirmed ECMA-262 gives ??=/||=/&&= the same NamedEvaluation rule as = (identifier target only), and compound arithmetic operators do not — the operator list is exactly right.
  • The save/restore of prev_decorator_class_name and the inner guards (EClass, should_lower_standard_decorators, EIdentifier left) are unchanged; only the operator predicate widened.
  • Checked the later BinNullishCoalescingAssign | BinLogicalOrAssign arm (the {}.field ??= v optimization) — it requires EDot on the left, disjoint from the EIdentifier case here.
  • Tests cover the three operators (method + class decorators), negative guards (explicit name, member target, nested, +=), and the scoping interaction with the base PR.
Extended reasoning...

Overview

One hunk in src/js_parser/visit/visit_binary.rs: the condition that sets p.decorator_class_name before visiting the right operand of an assignment is widened from e_.op == BinAssign to a matches! over BinAssign | BinNullishCoalescingAssign | BinLogicalOrAssign | BinLogicalAndAssign. Everything else in the block — the anonymous check, the EClass/should_lower_standard_decorators/EIdentifier guards, and the save/restore of the previous name — is unchanged. Four new test.concurrent cases in test/bundler/transpiler/es-decorators.test.ts exercise the fix and pin the negative cases.

Security risks

None. This is transpiler output correctness (the inferred .name of a lowered decorated class expression); no untrusted-input parsing, no allocation logic, no FFI/syscall/crypto surface.

Level of scrutiny

Low-to-medium. The change is a mechanical widening of an operator predicate at a single visitor site, and the spec is unambiguous that exactly these four assignment operators perform NamedEvaluation of an anonymous right-hand side against an identifier target. The existing = path already validates the mechanism; three sibling operators are being added to the same path with no new control flow. The PR is stacked on #38757, and the fourth test pins the scoping property that base PR provides (the recorded name must not shadow the outer variable inside the class body).

Other factors

  • The comment-cop bot's feedback was addressed (comment trimmed to one line stating which operators perform NamedEvaluation).
  • The tests include both positive coverage and negative guards (+=, member target, nested class, explicit class name), and were cross-checked against Node per the description.
  • I checked the later match arm for BinNullishCoalescingAssign | BinLogicalOrAssign (the {}.field ??= value HMR optimization): it requires e_.left to be EDot, so it cannot overlap with the EIdentifier case that records the name.
  • The separate maybe_keep_expr_symbol_name call in the BinAssign arm (for --keep-names) is intentionally left as-is; the description notes #35307 handles keep-names for these operators separately, so this PR stays scoped to the decorator path.

Comment thread src/ast/ast_result.rs
Comment thread src/ast/ast_result.rs
Comment thread src/ast/ast_result.rs
@robobun
robobun changed the base branch from farm/80aecbe5/decorator-inferred-class-name to main September 13, 2026 19:13
Comment thread src/ast/ast_result.rs
…r &&= after the target

Standard decorator lowering turns a class expression that has a class
decorator into a comma expression, so the engine cannot name the class
from its position. The parser records that name and the lowering passes
it to `__decorateElement`. The binary visitor recorded it only for `=`.
The logical assignment operators do the same NamedEvaluation of their
right side when the target is an identifier, so `a ??= @dec class {}`
had the name "" and the decorator got "" as `context.name`.
@robobun
robobun force-pushed the farm/4055ec3e/decorated-class-logical-assign-name branch from b498dfe to a858c8e Compare September 13, 2026 19:17
@robobun

robobun commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator Author

The base of this PR is now main. The stacked branch it was based on is no longer needed since #40833 changed the lowering. The diff is one hunk in src/js_parser/visit/visit_binary.rs plus tests. The review threads on src/ast/ast_result.rs came from the stale base and are 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.

LGTM — widening the decorator_class_name recording to the three logical-assignment operators matches the spec's NamedEvaluation set, and the negative cases (member LHS, +=, nested class, explicit name) confirm the guard stays tight.

What was reviewed:

  • The operator set in visit_binary.rs:124 against ECMA-262 NamedEvaluation — only =/??=/||=/&&= name the RHS; arithmetic compounds correctly excluded.
  • That the existing guards (is_anonymous_named, should_lower_standard_decorators, EIdentifier LHS) are unchanged, so member targets and named classes stay untouched.
  • The four new tests: they use the file's existing runDecorator helper, test.concurrent, assert stderr/stdout before exitCode, and cover the operator matrix plus scoping (no class-body binding introduced).
  • The earlier src/ast/ast_result.rs inline threads no longer apply — that file is not in the current diff after the rebase.
Extended reasoning...

Overview

The PR touches two files: src/js_parser/visit/visit_binary.rs (an 8-line condition widening) and test/bundler/transpiler/es-decorators.test.ts (four new test.concurrent cases in a new describe block). The Rust change extends the branch that records p.decorator_class_name — used by the standard-decorator lowering to give a lowered anonymous class its .name and decorator context.name — from BinAssign only to also include BinNullishCoalescingAssign, BinLogicalOrAssign, and BinLogicalAndAssign. The surrounding save/restore of prev_decorator_class_name and the inner guards (anonymous class, should_lower_standard_decorators, identifier LHS) are unchanged.

Security risks

None. This is a compile-time naming decision in the JS transpiler with no I/O, no untrusted-size arithmetic, and no memory-ownership changes. The tests spawn local subprocesses via the existing runDecorator helper and touch no network.

Level of scrutiny

Low-to-moderate. The change is spec-derived: ECMA-262 defines NamedEvaluation for exactly =, &&=, ||=, and ??= among the assignment operators, and not for arithmetic/bitwise compound assignments — the new matches! set is that exact list, and the one-line comment states the invariant. Per REVIEW.md's "fix the whole class" rule I checked for sibling sites: the PR description notes (and the code confirms) that logical-assignment operators are never lowered by the parser and the assignment-pattern default paths only see =, so visit_right_and_finish is the only visitor site involved. The maybe_keep_expr_symbol_name path further down handles the separate --keep-names concern and is out of scope here.

Other factors

Test coverage is thorough for the size of the fix: every operator, a chained Outer = Inner ??= …, an IIFE, member-decorator-only and accessor-only classes (engine-named path), explicit negative cases (Named, member LHS, nested-in-array, +=), and a scoping test proving the recorded name does not shadow the outer binding inside the class body. Tests follow repo conventions (existing file, existing runDecorator helper, test.concurrent, .toEqual on one object, stderr → stdout → exitCode ordering). CODEOWNERS does not cover either changed path. The branch was rebased since the last review round; the earlier inline threads on src/ast/ast_result.rs no longer correspond to any change in the current diff, and there are no outstanding CHANGES_REQUESTED reviews.

…lass names

The emitted code for `x ??= @dec class {}` changes, so cached output from
an older build must not be read back.

Also drop the test for classes that have only member decorators. Such a
class stays in place on main and the engine names it, so the test does
not exercise the change.

@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 transpiler-cache version bump added in the latest push completes this.

What was reviewed:

  • The matches! extension in visit_right_and_finish covers exactly the four assignment operators that spec NamedEvaluation on the RHS; compound arithmetic/bitwise ops and member-expression targets remain excluded, and prev_decorator_class_name save/restore is unchanged.
  • EXPECTED_VERSION 32→33 invalidates cached transpiler output as required for a codegen change.
  • Tests cover the operator matrix, negative cases (+=, named class, member target, nested), and confirm the recorded name does not become a class-body binding.
Extended reasoning...

Overview

The PR extends one condition in src/js_parser/visit/visit_binary.rs so that p.decorator_class_name is set for ??=, ||=, and &&= in addition to =, when the right operand is an anonymous class expression that will be lowered for standard decorators and the left operand is an identifier. It bumps EXPECTED_VERSION in src/jsc/RuntimeTranspilerCache.rs from 32 to 33, and adds three test.concurrent cases to the existing test/bundler/transpiler/es-decorators.test.ts describe block.

Security risks

None. This is a parser/lowering correctness fix affecting the .name and decorator context.name of anonymous class expressions. No untrusted-input parsing, no allocation/bounds arithmetic, no auth/crypto/permissions surface.

Level of scrutiny

Low-to-medium. The Rust change is a one-condition widening that mirrors the exact operator set the spec defines for NamedEvaluation of assignment RHS (only = and the three logical assignment operators; compound arithmetic/bitwise assignments do not). The surrounding save/restore of prev_decorator_class_name and the identifier-target / should_lower_standard_decorators guards are unchanged, so the blast radius is the four operators only. The cache bump satisfies the REVIEW.md rule that any cached-output change bumps the format version constant. No CODEOWNERS entries cover the touched paths.

Other factors

Since the previously reviewed push, commit 92dcfe7 added the transpiler-cache version bump and dropped one redundant member-decorator test case (its behavior — engine-named classes when only member decorators are present — is still pinned by the third test's Member case). Tests follow harness conventions (runDecorator uses tempDir/bunEnv/concurrent pipe drain, stderr asserted before exitCode, .toEqual on a single JSON object, test.concurrent). The negative test explicitly pins that +=, member targets, nested classes, and explicitly-named classes do not receive the target name. No outstanding human CHANGES_REQUESTED reviews; bot inline threads reference either this line or a file (src/ast/ast_result.rs) no longer in the diff. Bug-hunt exit reason was dry_streak.

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