Skip to content

fix(transpiler): rewrite private field refs in class field initializers with decorated accessor#28120

Open
robobun wants to merge 4 commits intomainfrom
claude/fix-28118-private-field-decorator-accessor
Open

fix(transpiler): rewrite private field refs in class field initializers with decorated accessor#28120
robobun wants to merge 4 commits intomainfrom
claude/fix-28118-private-field-decorator-accessor

Conversation

@robobun
Copy link
Copy Markdown
Collaborator

@robobun robobun commented Mar 14, 2026

Summary

  • When a class has a @decorated accessor, Bun lowers all private #fields to WeakMap-based helpers (__privateAdd/__privateGet). Phase 5 of the decorator lowering performs private access rewriting but was missing several data structures: constructor_inject_stmts, static_private_add_blocks, suffix_exprs, and nprop.initializer in the new_properties loop.
  • This left this.#field references inside non-decorated field initializers unrewritten, causing SyntaxError: Cannot reference undeclared private names at runtime.
  • Adds the missing rewrite calls for all four data structures.

Closes #28118

Test plan

  • Added regression test test/regression/issue/28118.test.ts with 4 test cases covering instance fields, direct references, static fields, and chained references
  • Tests fail with USE_SYSTEM_BUN=1 (confirming the bug) and pass with bun bd test (confirming the fix)
  • All 22 existing decorator tests pass
  • All 27 ES decorator tests pass
  • All 147 esbuild decorator tests pass

…rs with decorated accessor

When a class has a `@decorated accessor`, all private fields are lowered
to WeakMap-based helpers. The private access rewriting (Phase 5) was
missing `constructor_inject_stmts`, `static_private_add_blocks`, and
`suffix_exprs`, so `this.#field` references inside non-decorated field
initializers were left unrewritten, causing a SyntaxError at runtime.

Closes #28118

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Mar 14, 2026

Updated 7:57 AM PT - Mar 17th, 2026

@robobun, your commit ee771b9 has some failures in Build #39819 (All Failures)


🧪   To try this PR locally:

bunx bun-pr 28120

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

bun-28120 --bun

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 14, 2026

Walkthrough

Phase 5 of decorator lowering now rewrites private-access expressions for newly created properties including their initializers, and extends the rewrite pass to process constructor injection statements, static private-add blocks, suffix expressions, and post-decorating initializer expressions to ensure private names are consistently rewritten.

Changes

Cohort / File(s) Summary
Decorator lowering implementation
src/ast/lowerDecorators.zig
Phase 5: rewrite private-access expressions for newly created properties' values and initializers (nprop.initializer); extend rewrite pass to handle constructor_inject_stmts, all class_static_block static private-add blocks, and suffix_exprs; add post-decorating initializer rewrites.
Regression test suite
test/regression/issue/28118.test.ts
Add tests covering instance, static, and chained private field initializers and references in classes with a @decorated accessor to reproduce and prevent the private-name rewrite regression.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main fix: rewriting private field references in class field initializers when a decorated accessor is present.
Linked Issues check ✅ Passed The code changes directly address the root cause in issue #28118 by adding missing rewrite calls for constructor_inject_stmts, static_private_add_blocks, and suffix_exprs, and rewriting nprop.initializer in Phase 5, ensuring private field references in initializers are properly transformed.
Out of Scope Changes check ✅ Passed All changes are scoped to fixing the private field rewriting omission in decorator lowering. The changes focus on Phase 5 rewriting logic and adding regression tests, with no extraneous modifications.
Description check ✅ Passed The pull request provides a comprehensive description covering the problem, solution, test plan with verification steps, and related issue reference.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

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

Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — small, well-tested fix that adds missing private access rewrite calls following the same established patterns.

Extended reasoning...

Overview

This PR adds 6 lines to src/ast/lowerDecorators.zig in Phase 5 ("Rewrite private accesses") to call rewritePrivateAccessesInStmts/rewritePrivateAccessesInExpr on three data structures that were previously missed: constructor_inject_stmts, static_private_add_blocks, and suffix_exprs. A new test file with 4 regression tests is also added.

Security risks

None. This is a transpiler/AST transformation fix with no security-sensitive surface.

Level of scrutiny

Low scrutiny needed. The change is mechanical — it adds calls to existing rewrite functions following the exact same patterns already present for other data structures in the same Phase 5 block. The surrounding code already rewrites new_properties, instance_init_entries, static_init_entries, extracted_static_blocks, static_non_field_elements, instance_non_field_elements, static_field_decorate, instance_field_decorate, pre_eval_stmts, and prefix_stmts. This PR simply adds the three that were missed.

Other factors

The one bug report found is explicitly tagged as pre-existing (the nprop.initializer gap in the new_properties loop at lines 1088-1092), which predates this PR and is not modified by it. The PR includes 4 well-structured regression tests covering instance fields, direct references, static fields, and chained references. The PR description confirms all 22 existing decorator tests and all 147 esbuild decorator tests still pass.

Comment thread src/ast/lowerDecorators.zig
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — small, mechanical fix adding missing rewrite calls that follow the exact same pattern as adjacent code, with good regression tests.

Extended reasoning...

Overview

This PR adds 6 lines to src/ast/lowerDecorators.zig Phase 5 to apply rewritePrivateAccessesInStmts/rewritePrivateAccessesInExpr to three data structures that were previously missed: constructor_inject_stmts, static_private_add_blocks, and suffix_exprs. A new test file with 4 regression tests covers instance fields, direct references, static fields, and chained references.

Security risks

None. The change is purely in the transpiler/AST lowering layer and does not touch authentication, network, crypto, or permissions code.

Level of scrutiny

Low scrutiny is appropriate. The fix is minimal (6 lines), follows the exact same pattern as the surrounding rewrite calls (e.g., the existing loops over instance_init_entries, static_init_entries, extracted_static_blocks, etc.), and the intent is clear from both the code and the linked issue. No design decisions are involved.

Other factors

The only bug report is pre-existing (the new_properties loop missing nprop.initializer), which I already flagged in my previous inline comment and is unrelated to this PR's fix. CI build failures reference scripts/build/ci.ts infrastructure issues, not this change. CodeRabbit also found no actionable issues. The test plan is thorough, verifying the fix against 4 distinct scenarios plus confirming all 22 existing decorator tests and 147 esbuild decorator tests still pass.

Comment thread src/ast/lowerDecorators.zig
@alii
Copy link
Copy Markdown
Member

alii commented Mar 17, 2026

@robobun adopt

@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Mar 17, 2026

✅ All 17 test steps passed across all platforms. The only CI "failure" is upload-benchmark.mjs — an infrastructure script for benchmark uploads, unrelated to this PR.

…s to constructor for correct ordering

When lower_all_private is true, non-decorated public instance fields
with initializers must be moved into the constructor to preserve
evaluation order relative to lowered private fields.

Also adds the missing nprop.initializer rewrite in the new_properties
Phase 5 loop.
Comment thread src/ast/lowerDecorators.zig Outdated
Comment thread src/ast/lowerDecorators.zig Outdated
…only

Revert the Phase 4 change that moved public instance fields into
constructor_inject_stmts — this broke ordering relative to decorated
elements initialized in Phase 7. The nprop.initializer rewrite in
Phase 5 is still needed for completeness.

The public field ordering issue (class field initializer running before
constructor body where __privateAdd lives) is a pre-existing problem
that requires restructuring the init ordering across Phase 4/7, which
is out of scope for this fix.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ast/lowerDecorators.zig`:
- Line 1113: The loop over suffix_exprs.items currently only calls
rewritePrivateAccessesInExpr and thus leaves raw `this` in static accessor
initializers; update that loop to also call replace_this(p, expr,
class_name_ref) (or the existing replace_this helper) for each expr
before/alongside rewritePrivateAccessesInExpr so `this` is replaced with the
class_name_ref for static suffix initializers; reference the suffix_exprs.items
iteration, rewritePrivateAccessesInExpr, replace_this, and private_lowered_map
to locate and change the code similarly to how static blocks are handled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 74b3c2b9-0e55-4d75-ba9d-73be525d788e

📥 Commits

Reviewing files that changed from the base of the PR and between a74e23e and ee771b9.

📒 Files selected for processing (2)
  • src/ast/lowerDecorators.zig
  • test/regression/issue/28118.test.ts

Comment thread src/ast/lowerDecorators.zig
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

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

LGTM — the final diff is a small, mechanical addition of missing private-access rewrite calls in Phase 5, following existing patterns exactly.

Extended reasoning...

Overview

This PR adds 7 lines to src/ast/lowerDecorators.zig in the Phase 5 private-access rewriting block, plus a new regression test file (test/regression/issue/28118.test.ts) with 4 test cases. The Zig changes add missing rewritePrivateAccessesInExpr/rewritePrivateAccessesInStmts calls for: (1) nprop.initializer in the new_properties loop, (2) constructor_inject_stmts, (3) static_private_add_blocks, and (4) suffix_exprs. All follow the identical pattern used by existing rewrite calls immediately above them in the same code block.

Security risks

None. This is a transpiler fix that adds missing AST rewriting passes — no auth, crypto, network, or user-input handling is involved.

Level of scrutiny

Low scrutiny is appropriate. The changes are mechanical: the Phase 5 block already rewrites new_properties, instance_init_entries, static_init_entries, extracted_static_blocks, static_non_field_elements, instance_non_field_elements, static_field_decorate, instance_field_decorate, pre_eval_stmts, and prefix_stmts. The PR simply adds the three data structures that were missed (constructor_inject_stmts, static_private_add_blocks, suffix_exprs) plus the nprop.initializer field that was overlooked in the new_properties loop. The earlier problematic commit (a74e23e, moving public fields to constructor) was cleanly reverted in ee771b9.

Other factors

  • The test file covers instance fields, direct references, static fields, and chained references — all exercising the exact bug path.
  • CI passes across all platforms per the PR description.
  • All 22 existing decorator tests and 147 esbuild decorator tests pass.
  • The only bug report for this review is pre-existing: the public-to-private field reference pattern was already broken before this PR (SyntaxError), and remains a known limitation with a different error (TypeError). This PR correctly fixes its stated target (private-to-private references in initializers).
  • My previous review comments about Phase 4/Phase 7 ordering (inline comments on the reverted code) are no longer applicable since that code was removed in ee771b9.

Comment thread src/ast/lowerDecorators.zig
@robobun
Copy link
Copy Markdown
Collaborator Author

robobun commented Mar 18, 2026

@alii This PR is a complete fix, not half of a bigger change. Here's a breakdown:

Phase 4 (property loop) sorts class properties into various lists. For non-decorated private fields, it places __privateAdd(this, _weakmap, <init_val>) calls directly into:

  • constructor_inject_stmts (instance fields, line 798)
  • static_private_add_blocks (static fields, line 806)
  • suffix_exprs (static auto-accessors, line 878)

The <init_val> in these calls is the user's raw initializer expression, which may contain this.#field references.

Phase 5 rewrites this.#field__privateGet(this, _field) in all data structures — but was missing these three lists. That's the bug this PR fixes.

Phase 7 appends additional items to constructor_inject_stmts, but those are built from instance_init_entries whose initializers were already rewritten in Phase 5 (line 1093-1094). Phase 7 just wraps them in __runInitializers/__privateAdd helper calls with refs and numeric indices — no raw this.#field references exist in Phase 7's additions.

So the data flow is:

  • Phase 4: non-decorated fields → constructor_inject_stmts (with raw this.#field in init values) ← this is what was missed
  • Phase 5: rewrite all private accesses (now includes the three missing lists)
  • Phase 7: decorated fields from instance_init_entries (already rewritten) → append to constructor_inject_stmts

No double-rewriting risk either — rewritePrivateAccessesInExpr is a tree walk that only matches e_private_identifier nodes in the map, which are consumed on first rewrite.

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.

this.#field not rewritten in class field initializers when class has @decorated accessor

2 participants