Skip to content

Substitute this and inner class name in relocated static initializers during decorator lowering - #31922

Open
robobun wants to merge 2 commits into
mainfrom
farm/68d18d98/decorator-static-init-scope
Open

Substitute this and inner class name in relocated static initializers during decorator lowering#31922
robobun wants to merge 2 commits into
mainfrom
farm/68d18d98/decorator-static-init-scope

Conversation

@robobun

@robobun robobun commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Fixes #31917

Repro

function dec(v, c) { return v; }
const C = class Foo {
  static #m = function (tag) { return { tag }; };
  @dec static s = Foo.#m("s").tag;   // ReferenceError: Foo is not defined
};

this in the same position fails too (TypeError: Cannot read from private field), including in class declarations, because the relocated expression evaluates this at module level.

Cause

Standard-decorator lowering relocates static initializers out of the class body into the module-level chain that builds the class:

_class.s = __runInitializers(_init, 8, _class, __privateGet(Foo, _m).call(Foo, "s").tag)

Foo (the class expression's inner name binding) only exists inside the class body, and this is no longer the class. The lowering already has RewriteKind::ReplaceRef / ReplaceThis machinery for this (extracted static blocks get ReplaceThis), but it was not applied to the other relocated pieces, and the walker itself skipped several AST positions.

Fix

In src/js_parser/lower/lower_decorators.rs:

  1. Capture a ReplaceRef { old: inner name, new: _class } rewrite when swapping to the hoisted temp for class expressions, and apply the rewrites at all three relocation sites: extracted static blocks (ReplaceRef was missing), decorated static field/accessor initializers, and undecorated static auto-accessor initializers (both were missing). Class declarations keep their module-level name binding (and class decorators reassign it before static initializers run), so only ReplaceThis applies there.

  2. Cover the AST positions the rewrite_expr/rewrite_stmts walker skipped (flagged in review): nested classes (new rewrite_class, recursed for ReplaceRef; for ReplaceThis only the extends clause and computed keys, which evaluate in the enclosing scope), arrow/function parameter defaults including destructuring patterns (rewrite_args/rewrite_binding), computed object keys, await/yield operands, import() arguments, catch bindings, for-in/of heads, and function/class declarations inside static blocks.

this inside nested non-arrow functions and shadowing bindings stay untouched (ref identity plus the EFunction-style kind split; covered by tests).

Verification

New tests in test/bundler/transpiler/es-decorators.test.ts (relocated static initializers block, 15 tests): both issue repros, declaration + anonymous-expression this, decorated and undecorated static accessor, static block inner-name reference, class-decorator replacement visibility, one test per walker position family, plus two negative tests (nested-function this and a shadowing function name are not rewritten). 13 of the 15 fail before the fix, all pass after. Expected outputs validated against esbuild 0.21.5 lowering run under Node 24.

Existing decorator suites (es-decorators, es-decorators-esbuild, decorators, decorator-metadata, bundler_decorator_metadata) and transpiler.test.js all pass.

Related pre-existing problems in the accessor-only lowering path (native private names relocated out of class scope, accessor-vs-static-block evaluation order) are not substitution bugs and are tracked in #31921.

…initializers

Standard-decorator lowering relocates static field, accessor, and static
block initializers out of the class body. The relocated code kept
references to the class expression's inner name binding (ReferenceError
at module level) and to this (module this instead of the class).

Apply the existing RewriteKind::ReplaceThis / ReplaceRef passes at all
three relocation sites: extracted static blocks, decorated static
field/accessor initializers, and undecorated static auto-accessor
initializers.

Fixes #31917
@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

More reviews will be available in 7 minutes and 50 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: e90db097-7b55-4e1c-8f5a-3da3ed7c46ce

📥 Commits

Reviewing files that changed from the base of the PR and between a7839df and 98d210d.

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

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

@robobun

robobun commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

@robobun

robobun commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

CI status: the only failing test on both runs (builds 60964 and 60993, every red lane) is test/cli/install/bunx.test.ts "should handle package that requires node 24", which runs bun x --bun @angular/cli@latest --help against the live registry.

@angular/cli@22.0.0 (published 2026-06-03) raised its minimum Node requirement:

Node.js version v24.3.0 detected.
The Angular CLI requires a minimum Node.js version of v22.22.3 or v24.15.0 or v26.0.0.

so the subprocess exits 3 on every branch. It fails identically with bun 1.4.0-canary.1+55f6c899f, which predates this PR. Not caused by this diff (transpiler decorator lowering; the decorator suites pass). Either #31818 (upgrade reported Node.js version) or #31820 (pin @angular/cli in that test) unblocks it; this PR just needs a rerun or rebase once one lands.

Comment thread src/js_parser/lower/lower_decorators.rs
…lker

The rewrite_expr/rewrite_stmts walker used for relocated static
initializers skipped several AST positions, leaving this and inner
class name references unsubstituted there: nested classes, arrow and
function parameter defaults (including destructuring patterns), computed
object keys, await/yield operands, import() arguments, catch bindings,
for-in/of heads, and function/class declarations inside static blocks.

Add the missing arms. Nested classes and function declarations follow
the existing EFunction split: skipped for ReplaceThis (own this binding,
except extends clauses and computed keys, which evaluate in the
enclosing scope), recursed for ReplaceRef (ref identity keeps shadowing
bindings intact).

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

Thanks — 98d210d closes every walker gap I flagged (nested class, arg/binding defaults, computed keys, await/yield/import, SFunction/SClass, for-in/of init, catch binding) and each has a runtime test; I didn't find anything wrong in this revision, but the new rewrite_class/rewrite_binding traversal encodes enough JS scoping subtlety that someone who owns the decorator lowering should give it a final look.

Extended reasoning...

Overview

Two-commit PR against src/js_parser/lower/lower_decorators.rs plus ~230 lines of new tests in test/bundler/transpiler/es-decorators.test.ts. Commit c3d3e1d adds the core fix for #31917: capture a ReplaceRef { inner-name → _class } rewrite when a named class expression is hoisted, and apply ReplaceThis + ReplaceRef at the three points where static initializers are relocated out of the class body (decorated static fields/accessors, undecorated static auto-accessors, extracted static blocks). Commit 98d210d — pushed in response to my earlier review — extends the rewrite_expr/rewrite_stmts walker itself: new rewrite_class, rewrite_args, and rewrite_binding helpers, plus arms for EAwait/EYield/EImport, computed EObject keys, SFunction/SClass, for-in/for-of init, and catch bindings.

Security risks

None. Pure transpiler-output correctness; no I/O, auth, permissions, or untrusted-input parsing surface is touched.

Level of scrutiny

Medium-high. This is code generation in the JS transpiler — bugs here mis-compile user programs. The second commit in particular hand-encodes JS scoping rules (e.g. nested-class extends and computed keys evaluate in the enclosing this, while field initializers/method bodies/static blocks bind their own this; ReplaceRef recurses everywhere because it matches by Ref identity so shadowing is already safe). I traced each new arm against the spec semantics and they look right, and every gap I'd previously listed now has a dedicated end-to-end test that runs the lowered output and checks observable behaviour. But ~100 lines of new recursive AST traversal with per-position ReplaceThis-vs-ReplaceRef splits is past my threshold for auto-approval.

Other factors

  • My prior inline comment is fully addressed; nothing outstanding from me.
  • Bug-hunting pass on the new revision found nothing.
  • CI reds are the unrelated bunx.test.ts Angular-CLI registry break (and one streams-leak flake), already triaged in-thread; the decorator suites are green.
  • No CODEOWNERS entry for src/js_parser/.
  • Minor residual gap I noticed but don't think is worth blocking on: rewrite_class doesn't visit decorator expressions on the nested class or its members — a this/inner-name reference there would still be missed. Extreme edge case; fine as a follow-up alongside the #31921 items.

@robobun

robobun commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator Author

Re the residual gap note (decorator expressions on nested classes not visited by rewrite_class): verified it is unreachable. Standard decorators lower bottom-up, so a nested decorated class is already converted to its own (_dec = [...], _init = ..., _class = class {...}, ...) comma chain before the outer class's walker runs; any EClass reaching rewrite_class has empty ts_decorators, and the nested chain's decorator/member expressions sit in plain expression positions the walker already covers. Visiting ts_decorators there would be dead code, so I left it out.

Checking that did surface a different pre-existing miscompilation: the generated _class/_init/_dec temps of the nested and outer chains print as the same variables and clobber each other mid-expression (const C = class Foo { @dec static s = (class { @dec static x = Foo; }).x; } yields C.s === undefined). That exists on main independent of this PR (the baked canary produces the identical collision) and is a temp-naming problem, not a substitution one. Filed #31929 with the analysis.

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.

Decorator lowering: inner class name and this not substituted in relocated static field initializers of class expressions

1 participant