Skip to content

js_parser: give standard decorator lowering temporaries unique names - #38734

Open
robobun wants to merge 4 commits into
mainfrom
farm/55355bb8/decorator-temp-names
Open

js_parser: give standard decorator lowering temporaries unique names#38734
robobun wants to merge 4 commits into
mainfrom
farm/55355bb8/decorator-temp-names

Conversation

@robobun

@robobun robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #31930.

Fixes #31929
Fixes #28010
Fixes #28316
Fixes #29837

Problem

Fix

Background

  • The parser creates a Symbol per binding; the printer asks a renamer for each symbol's name. bun build uses NumberRenamer (appends numbers to resolve collisions) or MinifyRenamer; everything else uses NoOpRenamer, which prints Symbol.original_name as is. That is why generated bindings on the non-bundle path have to carry collision-free names themselves (generate_temp_ref documents this).
  • scope.generated is the list of parser-generated symbols belonging to a scope; renamers walk it for nested scopes. Part.declared_symbols lists the bindings each top-level part declares; it is the only source of names for the top level of an ESM file in NumberRenamer, and it also feeds the minifier's top-level slots.
  • A var emitted inside a block belongs to the enclosing function or module scope (Scope::kind_stops_hoisting), which is why the temporaries are registered there rather than in current_scope.
  • The lowering keeps per-class state in these temporaries at runtime: constructors call __runInitializers(_init, ...) and assign this[_computedKey], and lowered private members / accessors look their values up in the _<name> WeakMap. Any later class that reuses the name redirects those reads, which is why the symptoms range from syntax errors to silently wrong values.

no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_edgecase.test.ts

The temporaries the standard decorator / accessor lowering declares
(_init, _dec, _<Class>, the _<name> WeakMap of every lowered member, ...)
were created with fixed names. The runtime transpiler prints symbols by
original name, so they collided with identifiers of the same name in the
file and with each other, and they were never added to declared_symbols,
so the bundler's top-level renamer did not rename them either.

Register each temporary in the scope it hoists to and in declared_symbols
so the renamers handle it, and when no renamer runs pick file-unique names
after the visit pass, once every identifier the file uses has a symbol.
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

Next review available in: 24 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: 886957ad-18b2-4e2d-9cf8-e1d8a12f9f84

📥 Commits

Reviewing files that changed from the base of the PR and between e7460e3 and ec51ea7.

📒 Files selected for processing (5)
  • src/js_parser/lower/lower_decorators.rs
  • src/js_parser/p.rs
  • src/js_parser/parse/parse_entry.rs
  • test/bundler/bundler_edgecase.test.ts
  • test/bundler/transpiler/es-decorators.test.ts

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

@robobun

robobun commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

Status: ready for review (current revision ec51ea7).

Reproduced on main (9805144) and on the released 1.4.0 with plain .js files, no tsconfig:

The new tests in test/bundler/transpiler/es-decorators.test.ts (16) and test/bundler/bundler_edgecase.test.ts (9) fail on the unfixed build and pass with this branch; the decorator, transpiler, using and esbuild-port suites listed in the description pass. Supersedes #31930 (closed).

CI: build 97649 for the current head finished with 177 of 179 jobs passed and no test failures (the annotated ones are unrelated tests that passed on retry); the two darwin 14 aarch64 - test-bun shards expired on all four attempts without an agent, which is what makes the check red. That lane is not starting for any build in the queue at the moment, so re-running would not change the result.

@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 reviewed this PR and didn't find any bugs. Because it's a non-trivial change to the parser's decorator lowering (new post-visit naming pass, changed scope/declared_symbols registration for every temporary) and it supersedes #31930, a human look would still be worthwhile.

What was reviewed:

  • new_temp's hoisting-scope walk + declared_symbols append matches the existing using lowering pattern at p.rs:9057-9075, and .expect("oom") is the local convention there.
  • The removed expr_var_decls.push for _dec/_computedKey was a genuine duplicate — Phase 8 already extracts every SLocal binding from pre_eval_stmts into expr_var_decls (lower_decorators.rs:2515-2531).
  • name_decorator_temps runs after all append_part calls and is a no-op on the bundle path (only populated when !will_use_renamer()); accessor_storage_base gates on is_utf8() && is_identifier, fixing the accessor "x y" case.
Extended reasoning...

Overview

Fixes name collisions in the standard-decorator / accessor lowering (src/js_parser/lower/lower_decorators.rs). The lowering emits temporaries (_init, _dec, _<name> WeakMaps, ...) next to each class; previously those were created via new_sym with fixed names and only registered in current_scope.generated, so under the no-op renamer (runtime transpiler / --no-bundle) two distinct symbols printed as one identifier, and under the bundler renamer top-level ones were never renamed at all. The fix introduces new_temp, which registers each temporary in the nearest var-hoisting scope and in Part.declared_symbols, and a post-visit name_decorator_temps pass that assigns file-unique names when no renamer will run. It also fixes accessor "x y" producing an invalid var _x y and removes a double-push of pre-eval bindings into expr_var_decls. New state on P (decorator_temp_refs), one new call in _parse, and 15 new tests across the bundler and runtime-transpiler suites.

Security risks

None. Purely codegen naming; no input parsing, allocation sizing, or trust-boundary changes.

Level of scrutiny

Medium-high. This is core JS parser / lowering that ships in every bun run and bun build. The change is well-reasoned and heavily tested, but it introduces a new post-visit naming pass and changes how every decorator temporary is registered with the scope tree and declared_symbols — which feeds both NumberRenamer and MinifyRenamer. That is exactly the kind of subtle interaction where a maintainer familiar with the renamer contracts should sign off. It also supersedes an alternative approach in #31930, which is a design call.

Other factors

The implementation mirrors an existing pattern precisely: the hoisting-scope walk + is_top_level + scope.generated/declared_symbols registration is byte-for-byte the shape used by the using lowering at p.rs:9057-9079, including the .expect("oom") convention. The removed expr_var_decls pushes were verified to be duplicates of Phase 8's SLocal-extraction loop. name_decorator_temps is a no-op on the bundle path (decorator_temp_refs is only populated when !will_use_renamer()), and its call site in _parse is after all append_part calls so the symbol table is complete when it iterates self.symbols. The test coverage is thorough (statement/expression mode, sibling classes, sibling blocks, minified, cross-file, non-identifier keys, late globals) and each new test asserts exact stdout.

@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:52 AM PT - Aug 15th, 2026

@robobun, your commit ec51ea7 is building: #97649

…eiver temp

Move the hoisting-scope walk plus declared_symbols registration onto P
(var_hoisting_scope / declare_generated_binding) and use it from both the
using lowering, which had its own copy, and the decorator lowering. Route
the _obj temporary that captures a private call receiver through new_temp
as well; it was still printed under a fixed name in bundles. The remaining
fixed-name symbols take a 'static name so temporaries cannot bypass
new_temp.

Tests: method parameter shadowing a lowered member's storage, the receiver
temporary, the nested class expression from #31929, the cross-file shapes
from #30568, and decorated members in the accessor-only fixtures.
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/lower/lower_decorators.rs Outdated
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/lower/lower_decorators.rs
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/lower/lower_decorators.rs Outdated
Comment thread src/js_parser/p.rs Outdated
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 reviewed this PR and the automated bug-hunting pass found no issues. Because it reworks how the decorator lowering registers and names its generated bindings (new post-visit renaming pass, new declared_symbols/hoisting-scope registration shared with the using lowering, and a change to how the _obj receiver temp is declared), a human familiar with the parser/renamer architecture should still take a look.

What was reviewed:

  • new_temp / declare_generated_binding / var_hoisting_scope: registration in the hoisting scope + declared_symbols matches what NumberRenamer/MinifyRenamer consume; the using-lowering refactor to use the same helper preserves the previous behavior.
  • name_decorator_temps: reserving every symbol's original_name (not just the hoisting scope's) is deliberate — nested-scope shadowing is covered by a test; the two-pass clear-then-reserve avoids self-collision.
  • accessor_storage_base: checked that is_identifier gating rules out the var _x y case and that non-UTF-8 / computed keys fall back to _accessor_storage.
  • The _obj receiver temp switch from generate_temp_ref to new_temp + manual temp_refs_to_declare.push — verified drain_capture_temp_decls still declares it and it now participates in the collision-avoidance pass.
Extended reasoning...

Overview

This PR fixes name collisions in the standard-decorator / accessor lowering by (1) registering every lowering temporary in the enclosing var-hoisting scope and Part.declared_symbols so the bundler's renamers see them, and (2) adding a post-visit pass (name_decorator_temps) that assigns collision-free names when no renamer will run. It also extracts var_hoisting_scope / declare_generated_binding from LowerUsingDeclarationsContext into shared helpers, drops the ad-hoc per-class _dec2/_computedKey2/_accessor_storage0 counters, gates accessor-storage base names on is_identifier, and reroutes the _obj receiver-capture temp through the new mechanism. ~200 lines of Rust logic change plus ~370 lines of new tests across the runtime-transpiler and bundler suites.

Security risks

None. This is code-generation / symbol-naming logic in the parser; no untrusted-input parsing surface is added, no filesystem/network/crypto paths are touched.

Level of scrutiny

High. The decorator lowering is production code-generation on a hot path (_parse), and the fix threads through three interacting mechanisms (scope registration, declared_symbols, post-visit renaming) whose invariants ("top-level names come only from declared_symbols", "nested names come from scope.generated", "no renamer → original_name prints as-is") are documented but subtle. The using-lowering refactor is behavior-preserving but shares the new helper, so a regression there would affect an unrelated feature. This is well beyond a mechanical/config change and warrants a maintainer who knows the renamer contracts.

Other factors

  • The comment-cop bot left 11 unresolved inline flags on the new doc comments. They read as genuine function documentation rather than workaround justifications, but they are outstanding automated feedback the author may want to address or dismiss.
  • The last CI status comment (build #96808) reports failures on commit b04edc6; a follow-up commit a27d128 was pushed but no updated CI status is visible in the timeline.
  • Test coverage is thorough (12 runtime-transpiler tests + 7 bundler tests covering user-name collisions, intra-class collisions, cross-file, sibling blocks ± minify, nested-scope shadowing, non-identifier keys, and the receiver-temp case), and the description states the existing decorator/transpiler/esbuild-port suites pass.
  • No prior claude[bot] review on this PR.

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

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up after a self-review of the first revision (a27d128, fd7e30e):

@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 reviewed this and the automated bug hunt found nothing. Given the scope — a new post-visit naming pass, changes to how generated symbols land in scope.generated / Part.declared_symbols, and a refactor of the using lowering to share declare_generated_binding — a maintainer look would still be worthwhile, especially since this supersedes #31930 on approach.

What was reviewed:

  • new_temp / name_decorator_temps flow: temporaries append to p.declared_symbols during the visit inside append_part (which clears at entry and collects at exit), so the part picks them up.
  • Removal of the Phase-2 expr_var_decls.push for _dec / _computedKey — Phase 8 already extracts the bindings from pre_eval_stmts, so the old code was declaring them twice.
  • accessor_storage_base now gates on is_identifier, so accessor "x y" no longer produces var _x y.
  • The _obj receiver temp now goes through new_temp + temp_refs_to_declare, and drain_capture_temp_decls still declares it in the right body.
Extended reasoning...

Overview

Fixes standard-decorator/accessor lowering temporaries (_init, _dec, _<name> WeakMaps, etc.) colliding with each other and with user identifiers under both the no-renamer path (bun run, --no-bundle) and the bundler renamers. Touches src/js_parser/lower/lower_decorators.rs (replaces new_sym with new_temp for ~15 call sites, adds name_decorator_temps, accessor_storage_base, drops per-class counters), src/js_parser/p.rs (new decorator_temp_refs field, var_hoisting_scope, declare_generated_binding; refactors LowerUsingDeclarationsContext to use the new helper), and parse_entry.rs (one call after the visit pass). Adds 12 runtime tests and 7 bundler tests.

Security risks

None. Pure transpiler-output change; no I/O, network, auth, or user-input parsing surface added.

Level of scrutiny

High. Symbol naming and declared_symbols registration feed both bundler renamers and the printer directly; a mistake here miscompiles every file that uses a decorator or accessor. The change also touches the using lowering (behavior-preserving refactor to share declare_generated_binding), so two independent lowerings are affected. The design choice — a post-visit renaming pass over collected refs, rather than generate_temp_ref-style eager naming — is well-argued in the description (late-referenced globals, readable stack-trace names) but is exactly the kind of architectural decision a maintainer should sign off on, particularly since it supersedes the competing approach in #31930.

Other factors

  • CI: the last robobun status is ❌ on b04edc6, two commits behind head. No green status is visible for fd7e30e.
  • comment-cop has 7 open flags on the current head (doc comments on new_temp, name_decorator_temps, declare_generated_binding, etc.). These read as necessary API documentation rather than workaround justifications, but a maintainer should confirm.
  • Test coverage is thorough: statement vs. expression classes, user-declared collisions, late globals, nested-scope shadowing, sibling classes, sibling blocks with and without --minify-identifiers, cross-file bundling, non-identifier accessor keys, and the #31929 nested-static-initializer case. The description states all fail on the unfixed build.
  • Spot-checked: the removed Phase-2 expr_var_decls pushes were genuinely duplicated by Phase 8's SLocal walk over pre_eval_stmts; append_part clears declared_symbols before visiting and collects after, so new_temp's appends land in the right part; new_fixed_name_sym correctly retains the three symbols whose original name is semantically meaningful (class .name, setter param, arguments).

…8316 and #29837

Adds the shapes from #31930 that were not covered yet: decorated field
initializers in a base class and a subclass (#28010), two decorated
classes in one bundled file (#28316), accessor storage in a base class
and a subclass (#29837), the _m / _m_fn temporaries of decorated private
methods in two classes, and a class statement named init, whose _init
binding shares its base name with another class's initializer array.
@robobun

robobun commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator Author

Cross-checked this branch against the other open fixes for the same decorator lowering bugs (debug build of fd7e30e):

Pushed ec51ea7 with the test shapes from #31930 that were not covered yet: the #28010 field initializers and the #29837 accessor storage in a base class and a subclass, the _m / _m_fn temporaries of decorated private methods in two classes, a class statement named init (its _init binding shares a base name with the initializer arrays), and the #28316 shape as a bundler test. All five fail on the unfixed build and pass here; the description's test counts are updated.

@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 reviewed this PR and didn't find any bugs. It's a substantial change to the parser's symbol registration and adds a new post-visit naming pass, so a human look at the renamer/scope interactions would still be worthwhile.

Checked: name_decorator_temps clears the temps' own names before scanning p.symbols so they don't reserve themselves, and the suffix search skips existing _x2-style user names correctly. Phase 8 already pushes pre_eval_stmts bindings into expr_var_decls, so dropping the inline pushes for dec_ref/key_ref removes a duplicate rather than losing a declaration. The using refactor is behavior-equivalent (the dropped ensure_unused_capacity is replaced by per-item append().expect("oom")). accessor_storage_base now gates on is_identifier, closing the var _x y hole.

Extended reasoning...

Overview

This PR fixes standard-decorator lowering temporaries (_init, _dec, _<name> WeakMaps, _obj, etc.) that previously had fixed names and collided with user identifiers and with each other, both under bun run (no renamer) and bun build (renamer didn't see them). It touches lower_decorators.rs (~150 changed lines), adds two helpers on P (var_hoisting_scope, declare_generated_binding) that the using lowering now shares, adds a decorator_temp_refs field, and inserts a post-visit name_decorator_temps() call in parse_entry.rs. 20 new tests across the transpiler and bundler suites cover user-name collisions, intra-class collisions, cross-class/cross-file collisions, minified sibling blocks, and non-identifier accessor keys.

Security risks

None. This is code-generation naming; the inputs are already-parsed AST symbols and the output is identifier strings. No untrusted-size arithmetic, no filesystem/network paths.

Level of scrutiny

High. lower_decorators.rs runs on every file that uses standard decorators or accessor, and the change adds a new global naming pass that runs after the visit and mutates symbol.original_name for a subset of symbols. It also changes where generated bindings are registered (hoisting scope + declared_symbols instead of current_scope.generated alone), which interacts with NumberRenamer, MinifyRenamer, and per-part declared_symbols collection during tree-shaking. The using lowering refactor is behavior-preserving but touches working code. These are exactly the interactions the PR description spends most of its length justifying, and a maintainer who knows renameSymbolsInChunk should confirm the is_top_level / scope.generated registration is right for every renamer.

Other factors

The description is unusually thorough (mechanism, why the superseded #31930 was insufficient, which tests fail on which build), the comment-cop bot flags were addressed in fd7e30e, and the test matrix follows REVIEW.md's variant guidance (statement/expression, sibling blocks ± minify, cross-file, nested-in-static-initializer, non-identifier keys). No human reviewer has looked at it yet; CI build #97649 is in progress.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant