Skip to content

js_parser: keep [[Define]] semantics for decorated class fields - #35537

Closed
robobun wants to merge 5 commits into
mainfrom
farm/1efbb3ab/decorated-field-define-semantics
Closed

robobun wants to merge 5 commits into
mainfrom
farm/1efbb3ab/decorated-field-define-semantics

Conversation

@robobun

@robobun robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #20664

Reproduction

// bun run dec.ts  (standard TC39 decorators, no tsconfig)
const seen: string[] = [];
function tag(_v: any, ctx: any) { seen.push("tag:" + ctx.kind + ":" + String(ctx.name)); }
class Base { set p(v: any) { seen.push("SETTER-INVOKED:" + v); } get p() { return "getter"; } }
class C extends Base { @tag p: any = "field-init"; q: any = "plain-field"; }
const c: any = new C();
console.log({ seen, p: c.p, ownP: Object.prototype.hasOwnProperty.call(c, "p"),
                          ownQ: Object.prototype.hasOwnProperty.call(c, "q") });

Before:

{ seen: ['tag:field:p', 'SETTER-INVOKED:field-init'], p: 'getter', ownP: false, ownQ: true }

tsc 5.9 / esbuild 0.28 / Node (spec):

{ seen: ['tag:field:p'], p: 'field-init', ownP: true, ownQ: true }

With a getter-only inherited accessor, new C() threw TypeError: Attempted to assign to readonly property.. The same divergence reproduced under experimentalDecorators: true.

Cause

A decorated public field was lowered to this.p = init (instance) or C.p = init (static), i.e. [[Set]] semantics, while the undecorated field in the same class stayed as a native class field ([[Define]]). Adding a decorator silently flipped the field from [[Define]] to [[Set]].

  • Standard decorators: lower_decorators.rs emitted Stmt::assign(this.p, __runInitializers(...)) into the synthesized constructor.
  • Legacy decorators: p.rs::lower_class moved each decorated field into the constructor as this.p = init (or after the class as C.p = init for static) and dropped it from the class body.

Fix

  • Standard decorators: emit __publicField(this, key, __runInitializers(...)) / __publicField(C, key, ...) instead of plain assignment. __publicField uses Object.defineProperty when the key is inherited, matching esbuild's lowering.
  • Legacy decorators: leave decorated fields in the class body as native class fields (matching tsc and esbuild under useDefineForClassFields: true, which Bun assumes). The __legacyDecorateClassTS call is unchanged. The now-unreachable constructor-injection machinery is removed. Decorated declare/abstract fields are still dropped from the body after their decorator call is recorded.

The member_target helper became dead and is removed.

Tests

New tests cover instance, static, and getter-only inherited accessors for both decorator flavors. Two existing tests encoded the old [[Set]] behaviour and have been updated:

  • decorators random: the Emoji/StringAppender pattern (property decorator installs a prototype accessor and expects the initializer to call it) does not work under tsc or esbuild with useDefineForClassFields: true either; the test now asserts [[Define]] semantics and that the decorator calls still happen with the right targets/keys.
  • The constructor-splice perf test now asserts the new output shape (fields stay in the class body) since the code path it guarded no longer exists.
bun bd test test/bundler/transpiler/decorators.test.ts          # 26 pass
bun bd test test/bundler/transpiler/es-decorators.test.ts       # 62 pass
bun bd test test/bundler/transpiler/es-decorators-esbuild.test.ts test/bundler/transpiler/decorator-metadata.test.ts test/bundler/bundler_decorator_metadata.test.ts   # 154 pass

[review] gate passed · iteration 1 · 4 files touched

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

test/bundler/transpiler/decorators.test.ts:
(pass) decorator order of evaluation [80.10ms]
(pass) decorator factories order of evaluation [78.22ms]
(pass) parameter decorators [60.51ms]
396 |       });
397 |     };
398 |   }
399 | 
400 |   const iceCream = new IceCreamComponent();
401 |   expect(iceCream.flavor).toBe("vanilla");
                                ^
error: expect(received).toBe(expected)

Expected: "vanilla"
Received: "🍦 vanilla 🍦"

      at <anonymous> (/workspace/bun/test/bundler/transpiler/decorators.test.ts:401:27)
(fail) decorators random [50.81ms]
(pass) class field order [17.27ms]
(pass) changing static method [18.18ms]
(pass) class extending from another class [29.91ms]
(pass) decorated field initializers [18.29ms]
700 |     ownP: Object.prototype.hasOwnProperty.call(c, "p"),
701 |     ownQ: Object.prototype.hasOwnProperty.call(c, "q"),
702 |     ownNoInit: Object.prototype.hasOwnProperty.call
... (truncated)

release without fix: 16 FAILED
bun test v1.4.0-canary.1 (1498d7b77)

test/bundler/transpiler/decorators.test.ts:
(pass) decorator order of evaluation [1.60ms]
(pass) decorator factories order of evaluation [0.42ms]
(pass) parameter decorators [0.36ms]
396 |       });
397 |     };
398 |   }
399 | 
400 |   const iceCream = new IceCreamComponent();
401 |   expect(iceCream.flavor).toBe("vanilla");
                                ^
error: expect(received).toBe(expected)

Expected: "vanilla"
Received: "🍦 vanilla 🍦"

      at <anonymous> (/workspace/bun/test/bundler/transpiler/decorators.test.ts:401:27)
(fail) decorators random [0.73ms]
(pass) class field order [0.14ms]
(pass) changing static method [0.14ms]
(pass) class extending from another class [0.19ms]
(pass) decorated field initializers [0.11ms]
700 |     ownP: Object.prototype.hasOwnProperty.call(c, "p"),
701 |     ownQ: Object.prototype.hasOwnProperty.call(c, "q"),
702 |     ownNoInit: Object.prototype.hasOwnProperty.call(c, "noInit"),
703 |     s: (C as any).s,
704 |     ownS: Object.prototype.hasOwnProperty.call(C, "s"),
705 |   }).toEqual({
           ^
error: expect(received).toEqual(expected)

  {
-   "ownNoInit": true,
-   "ownP": t
... (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/bundler/transpiler/decorators.test.ts test/bundler/transpiler/es-decorators.test.ts
bun test v1.4.0 (364c48a30)

test/bundler/transpiler/decorators.test.ts:
(pass) decorator order of evaluation [42.20ms]
(pass) decorator factories order of evaluation [38.54ms]
(pass) parameter decorators [31.56ms]
(pass) decorators random [67.17ms]
(pass) class field order [9.05ms]
(pass) changing static method [9.89ms]
(pass) class extending from another class [15.67ms]
(pass) decorated field initializers [11.93ms]
(pass) decorated field keeps [[Define]] semantics over an inherited accessor [18.72ms]
(pass) decorated field does not throw on an inherited getter-only accessor [12.52ms]
(pass) only class decorator [8.06ms]
(pass) decorators with different property key types [20.86ms]
(pass) only property decorators [15.30ms]
(pass) only argument decorators [5.88ms]
(pass) no decorators [5.22ms]
(pass) constructor statements > with parameter properties [20.48ms]
(pass) constructor statements > class expressions (no decorators) [19.46ms]
(pass) constr
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped)
  target       linux-x64-gnu
  build type   Release
  build dir    ./build/release
  revision     364c48a301
  features     baseline

22 deps, 108 codegen, 1171 objects in 955ms

ninja: Entering directory `/workspace/bun/build/release'
[1/1109] gen ErrorCode+*.h
[2/1109] cc obj/vendor/brotli/c/common/constants.c.o
[3/1109] cc obj/vendor/brotli/c/common/shared_dictionary.c.o
[4/1109] cc obj/vendor/zstd/lib/decompress/huf_decompress_amd64.S.o
[5/1109] cc obj/vendor/brotli/c/common/context.c.o
[6/1109] cc obj/vendor/brotli/c/common/transform.c.o
[7/1109] fetch lsquic
[lsquic] up to date
[8/1109] cc obj/vendor/brotli/c/dec/bit_reader.c.o
[9/1109] cc obj/vendor/brotli/c/common/platform.c.o
[10/1109] cc obj/vendor/brotli/c/dec/state.c.o
[11/1109] cc obj/vendor/brotli/c/dec/huffman.c.o
[12/1109] cc obj/vendor/brotli/c/common/dictionary.c.o
[13/1109] gen generated_host_exports.rs
generated_host_exports.rs: 91 exports (host=3, lazy=10, generic=78, rust=0); 240 extern-C blocks audited
[14/1109] cc obj/vendor/zstd/lib/dictBuilder/fastcover.c.o
[15/1109] gen cpp.rs (cppbind)
[16/1109] gen ZigGe
... (truncated)
diff hotspot
src/js_parser/lower/lower_decorators.rs       |  60 ++---
 src/js_parser/p.rs                            | 173 +--------------
 test/bundler/transpiler/decorators.test.ts    | 307 ++++++++++++++------------
 test/bundler/transpiler/es-decorators.test.ts |  69 +++++-
 4 files changed, 248 insertions(+), 361 deletions(-)

gate history · 1 passed · 1 rejected · iteration 1

evidence per changed file
file                                           reads  edits  tests
src/js_parser/lower/lower_decorators.rs            6      5      0
src/js_parser/p.rs                                 6     10      0
test/bundler/transpiler/decorators.test.ts         5      8      0
test/bundler/transpiler/es-decorators.test.ts      1      2      0

A decorated public class field was lowered to a plain assignment
('this.p = init' for instance, 'C.p = init' for static), which uses
[[Set]] semantics and invokes any inherited setter instead of creating
an own data property. An undecorated field in the same class kept the
native class-field [[Define]] semantics, so adding a decorator silently
flipped the semantics and could either read back the wrong value or
throw 'Attempted to assign to readonly property' on a getter-only
inherited accessor. tsc and esbuild both keep [[Define]] for decorated
fields.

Standard (TC39) decorators: emit '__publicField(this, key, value)'
instead of 'this[key] = value' for lowered instance and static fields.

Legacy experimentalDecorators: keep decorated fields in the class body
as native class fields instead of moving them into the constructor. The
'__legacyDecorateClassTS' call is unchanged. The constructor-injection
path this made dead has been removed. Decorated 'declare'/'abstract'
fields are still dropped from the body after their decorator call is
recorded.
@coderabbitai

coderabbitai Bot commented Jul 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 20 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5f748607-bd29-4a3a-aa08-44f31937eb7f

📥 Commits

Reviewing files that changed from the base of the PR and between 1edfe55 and a4c740d.

📒 Files selected for processing (2)
  • src/js_parser/lower/lower_decorators.rs
  • src/js_parser/p.rs

Walkthrough

Changes

Decorated field lowering

Layer / File(s) Summary
Class lowering and statement assembly
src/js_parser/p.rs
Decorated declare and abstract fields are treated as type-only, and final statements are assembled from the base class statement and decorator-generated statements.
Runtime field initialization
src/js_parser/lower/lower_decorators.rs
Decorated public static and instance fields use __publicField calls with the appropriate receiver, key, and initializer.
Decorator behavior validation
test/bundler/transpiler/decorators.test.ts, test/bundler/transpiler/es-decorators.test.ts
Tests cover define semantics, inherited accessors, own properties, decorator ordering, static fields, and field initializer placement.

Possibly related PRs

  • oven-sh/bun#34538: Both changes modify lower_class handling of instance member assignments around decorated field lowering.

Suggested reviewers: jarred-sumner

🚥 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 summarizes the main change: preserving [[Define]] semantics for decorated class fields.
Description check ✅ Passed The description covers the problem, fix, reproduction, and verification, even though it uses different headings than the template.

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

Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/p.rs Outdated
@robobun

robobun commented Jul 25, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:08 AM PT - Jul 25th, 2026

@robobun, your commit a4c740d is building: #80227

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. Decorated properties are being removed #20664 - Decorated but uninitialized class fields were stripped from the class body during transpilation because they were lowered to constructor assignments; this PR keeps them as native class fields with [[Define]] semantics, preserving own properties.

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #20664

🤖 Generated with Claude Code

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

No bugs found, but this is a user-visible transpiler semantics change that warrants a human look — it flips decorated public fields from [[Set]] to [[Define]], deletes ~150 lines of legacy constructor-injection lowering in p.rs, and rewrites the decorators random test (which previously certified the old [[Set]] behavior) to assert the opposite.

What was reviewed:

  • __publicField/__defNormalProp in runtime.js — uses defineProperty when the key is inherited, so [[Define]] is preserved.
  • constructor_function in p.rs is still live (used for emitDecoratorMetadata at ~L6640); member_target has no remaining callers.
  • Computed-key evaluation count is unchanged vs. the old member_target path (key expr embedded once either way).
Extended reasoning...

Overview

The PR changes how decorated public class fields are lowered in both decorator flavors. For standard (TC39) decorators, lower_decorators.rs now emits __publicField(this|C, key, __runInitializers(...)) instead of a plain assignment. For legacy (experimentalDecorators) decorators, p.rs::lower_class no longer moves decorated fields into a synthesized constructor / post-class assignment — they stay as native class fields, and the ~150-line constructor-splice machinery (including synthesized super(...arguments)) is deleted. The now-unused member_target helper is removed. Tests are added for inherited-accessor shadowing (instance/static/getter-only) in both flavors, and two existing tests that encoded [[Set]] semantics are rewritten.

Security risks

None identified. This is compile-time AST lowering; no untrusted input reaches new allocation or syscall paths.

Level of scrutiny

High. This is a deliberate, user-visible behavior change to the transpiler: code that previously invoked an inherited setter on a decorated field will now shadow it with an own data property. The PR's argument (matches tsc 5.9 / esbuild / spec under useDefineForClassFields: true, which Bun assumes) is sound, but a maintainer should confirm that assumption still holds for the legacy-decorator path and that the rewritten decorators random test isn't losing coverage of something else (it dropped the StringAppender accessor-via-decorator pattern entirely and replaced it with a target/key recorder).

Other factors

  • There are three outstanding comment-cop inline comments asking the added multi-line comment blocks be removed; those haven't been addressed yet.
  • I verified constructor_function remains live for emitDecoratorMetadata, member_target has no remaining references, and __publicField → __defNormalProp uses Object.defineProperty on inherited keys (matching esbuild's helper).
  • The old code embedded prop.key once via member_target; the new code embeds it once via call_rt, so computed-key side-effect evaluation count is unchanged.
  • The removed constructor-injection path also handled the no-constructor / extends case by synthesizing constructor(){super(...arguments); ...}. Leaving fields as native class fields makes that unnecessary, but it interacts with parameter properties and emitDecoratorMetadata — worth a maintainer glance.

Comment thread src/js_parser/p.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.

I didn't find any bugs, but this is a user-visible semantic change to the transpiler (decorated fields switch from [[Set]] to [[Define]]) that removes ~150 lines of legacy-decorator lowering and rewrites existing tests to encode the new behavior — worth a human confirming the useDefineForClassFields: true assumption and the test rewrites.

What was reviewed:

  • __publicField in src/runtime.js — confirmed it uses defineProperty when key in obj, so the standard-decorator change gives [[Define]] semantics.
  • constructor_function in p.rs::lower_class is still live (used for design:paramtypes metadata below the removed block).
  • PropertyKind::Declare/Abstract only reach lower_class when decorated (parse_property.rs:432,458), so the new continue doesn't drop undecorated fields.
  • The removed member_target helper has no remaining callers.
Extended reasoning...

Overview

This PR changes how decorated public class fields are lowered so they preserve [[Define]] semantics instead of silently falling back to [[Set]]. It touches two lowering paths: standard TC39 decorators in src/js_parser/lower/lower_decorators.rs (swap this.k = ... / C.k = ... for __publicField(this, k, ...)), and legacy TS decorators in src/js_parser/p.rs::lower_class (stop moving decorated fields into a synthesized constructor / post-class assignments; leave them as native class fields and only drop declare/abstract bodies). The now-dead member_target helper and ~120 lines of constructor-injection machinery are removed. Tests in decorators.test.ts and es-decorators.test.ts add coverage for inherited accessors (instance, static, getter-only) and rewrite two existing tests that encoded the old [[Set]] behavior.

Security risks

None. This is a transpiler output-shape change; no untrusted input parsing, auth, crypto, or filesystem paths are affected.

Level of scrutiny

High. The change is in js_parser, which runs on every transpiled file, and it is a deliberate user-observable behavioral change (not a pure bugfix): any code that relied on a decorated field triggering an inherited setter will now behave differently. The decorators random test was substantially rewritten — the StringAppender prototype-accessor pattern it exercised no longer works under [[Define]] semantics, and the PR description argues (correctly, per my read) that tsc/esbuild with useDefineForClassFields: true behave the same way. A maintainer should sign off on that reasoning and on removing the constructor-splice machinery wholesale rather than gating it.

Other factors

  • I confirmed __publicField → __defNormalProp in src/runtime.js:209-215 uses Object.defineProperty when the key is on the prototype chain, so the standard-decorator fix does what it claims.
  • constructor_function is still read at p.rs:6634 for emitDecoratorMetadata design:paramtypes, so removing the instance-member injection didn't leave it dead.
  • The new Declare | Abstract drop is scoped correctly: parse_property.rs only returns those kinds when ts_decorators.len() > 0, so undecorated fields never hit that branch.
  • One comment-cop thread on p.rs is still open; the 3-line explanatory comment at lines 6606-6608 may be what it's flagging, though it reads as a legitimate why-comment to me.
  • No prior review from me on this PR.

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

I didn't find any bugs. The fix looks correct — __publicField matches esbuild's helper, the declare/abstract drop is verified against parse_property, and the removed constructor-injection machinery is genuinely dead. But this is a user-visible semantics change to decorator lowering (some legacy prototype-accessor patterns will stop working, as the rewritten decorators random test shows), so a human should sign off on the behavior change and the two rewritten tests.

What was reviewed:

  • __publicField → __defNormalProp in runtime.js uses defineProperty when key in obj, giving [[Define]] semantics as claimed.
  • parse_property.rs confirms undecorated declare/abstract return None, so the new PropertyKind::Declare | Abstract skip only handles the decorated case.
  • constructor_function and arguments_str are still live for emit_decorator_metadata / elsewhere; member_target had no other callers.
Extended reasoning...

Overview

The PR fixes #20664 by switching decorated public class fields from [[Set]] to [[Define]] semantics in both lowering paths: standard (TC39) decorators in lower_decorators.rs now emit __publicField(this|C, key, __runInitializers(...)) instead of a plain assignment, and legacy (experimentalDecorators) in p.rs::lower_class now leaves decorated fields in the class body as native class fields instead of moving them into a synthesized constructor as this.x = init. About 170 lines of constructor-injection machinery (the instance_members/static_members collection, constructor synthesis, and the member_target helper) are removed. A new explicit PropertyKind::Declare | Abstract check drops those from the class body after their decorator call is recorded — verified against parse_property.rs, which only preserves declare/abstract properties when they carry decorators.

Security risks

None. This is transpiler output-shape work with no I/O, auth, or untrusted-input parsing changes.

Level of scrutiny

High. The transpiler is a critical hot path, and this is a user-visible semantics change: decorated fields that previously triggered inherited setters (the Emoji/StringAppender pattern in the old decorators random test) now shadow them with own data properties. The PR argues — correctly, and I verified __publicField's definition in runtime.js — that this matches tsc/esbuild under useDefineForClassFields: true and the spec. But it will break real code that relied on the old behavior, and two existing tests were rewritten to encode the new semantics rather than the old. Per the repo's review guidance ("never silently weaken, skip, or delete an existing test"), the rewrites are justified in the PR description, but a maintainer should confirm they agree the old test was certifying a bug rather than an intentional compat choice.

Other factors

  • I confirmed constructor_function is still used (line ~6632, emit_decorator_metadata) and arguments_str is still used elsewhere (line 4532), so no dead code was left behind.
  • The instance-field __publicField change mirrors the existing __privateAdd branch immediately above it in both the static and instance paths — consistent with local convention.
  • Test coverage is good: instance/static/getter-only × both decorator flavors, plus the esbuild decorator suite (154 tests) still passes per the PR body.
  • The perf test rewrite is reasonable since the O(M×N) splice path it guarded no longer exists; the replacement asserts the new output shape instead.
  • All comment-cop threads are marked 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.

I didn't find bugs, but this changes user-observable transpilation semantics (decorated fields flip from [[Set]] to [[Define]]) and removes the legacy constructor-injection path, so it's worth a human confirming the direction.

Reviewed: the __publicField swap in lower_decorators.rs (matches the existing __privateAdd sibling arm and the runtime helper); the removed p.rs splice machinery — constructor_function is still consumed by emit_decorator_metadata, and member_target has no remaining callers; the decorators random rewrite encodes what tsc/esbuild produce under useDefineForClassFields: true; new tests cover instance/static/no-init/getter-only for both decorator flavors.

Extended reasoning...

Overview

The PR touches four files: two parser lowering paths (src/js_parser/lower/lower_decorators.rs for TC39 standard decorators, src/js_parser/p.rs::lower_class for TS legacy decorators) and their two test files. For standard decorators, this.p = __runInitializers(...) becomes __publicField(this, key, __runInitializers(...)), giving [[Define]] semantics via the existing runtime helper. For legacy decorators, ~170 lines that moved decorated fields into a synthesized constructor as this.p = init are deleted; decorated fields now stay in the class body as native class fields, with only declare/abstract kinds still stripped after their decorator call is recorded. The now-dead member_target helper is removed.

Security risks

None. This is pure transpiler output-shape work with no I/O, network, auth, or untrusted-input parsing changes.

Level of scrutiny

High — this is a deliberate semantics change to how every decorated public class field is emitted, in both decorator modes. The direction is well-argued (matches spec, tsc 5.9, esbuild 0.28, and Bun's own assumption per the // our version assumes useDefineForClassFields is true comment in visit/mod.rs), and the fix is at the right layer. But it will change behavior for user code that relied on the old [[Set]] lowering — the Emoji/StringAppender pattern in the rewritten test is exactly that pattern (property decorator installs a prototype accessor, initializer flows through it). A maintainer should sign off that the ecosystem-compat trade-off is intended, and that Bun does not want to gate this on the use_define_for_class_fields parser option that exists in parse_entry.rs.

Other factors

  • Two existing tests were rewritten to encode the new behavior rather than the old. The PR description justifies each, and the rewrites still assert the strongest available invariants (own-property checks, exact decorator-call ordering across instance/static/symbol/computed keys), so coverage is not weakened — but per REVIEW.md, changing what a test asserts warrants a stated reason and human confirmation.
  • The removed constructor-splice perf test guarded a code path that no longer exists; its replacement asserts the new output shape instead of a timing ratio, which is reasonable.
  • I confirmed constructor_function is still read (for design:paramtypes metadata emission), member_target has no remaining references, and __publicField is a pre-existing runtime helper (src/runtime.js, src/ast/runtime.rs).
  • All comment-cop threads on the PR are marked resolved; CI build #80150 was still in progress at the time of the last timeline update.

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

The diff is green on every lane that built. Remaining CI failures in build #80227 are unrelated to this change:

  • build-bun step failed outside the test runner on 4 lanes (linux x64-android, linux aarch64-android, linux x64-asan, darwin x64), marked [pre-existing] (also failing on main).
  • test-fastutf8stream-reopen.js (debian aarch64) and fetch-leak.test.ts (windows aarch64) each passed on retry ([flaky]).

The two decorator test files pass on every lane that ran them. Ready for review.

@robobun

robobun commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

A maintainer asked for one change that covers every experimental decorator lowering bug in this area. #40830 does that, and it includes the fix this PR makes (decorated fields stay in the class body, computed keys are captured once, parameter decorators use the enclosing scope, decorators that read a private name run in a static block, export default @dec class, accessor lowering). If #40830 lands, this PR can be closed.

@robobun

robobun commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #40830 (experimental decorators) and #40833 (standard decorators). Closing in favor of those two PRs.

I ran the tests of this PR against a build of each:

@robobun robobun closed this Aug 29, 2026
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.

Decorated properties are being removed

2 participants