Repository navigation
Conversation
Destructuring a macro-returned object with the binding keys in a different order than the object's own properties dropped bindings: visit_binding_and_expr_for_macro compacts the property array to only the referenced keys, but did so with an in-place copy props[end] = props[query.i] while still iterating the binding pattern. When end < query.i that copy stomps whatever property was at end before a later binding key could look it up, so as_property(name) fails and the emitted initializer is missing the key. Swap the two slots instead so every property stays reachable until all lookups are done. Skip the swap when query.i < end, which only happens when the binding repeats a key the compaction already placed. Fixes #9613.
|
Warning Review limit reached
Next review available in: 24 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Duplicate of #35574, which was opened first for the same bug with a two-pass bitmap approach. Closing this in favour of that PR; left a note there about the swap-based alternative and the |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
LGTM — swap-instead-of-copy is the right fix and drops an unsafe block.
What was reviewed:
- Traced all six 3-key permutations and the
{d, b}subset case through the new swap loop; each produces the correct kept prefix. - Verified
as_propertyis a linear first-match scan, so swapped-to-tail entries stay reachable; thequery.i < endguard only fires on repeated binding keys and correctly avoids pulling an unmatched property into the prefix. - Checked the tests follow harness conventions (
tempDir,bunEnv, concurrent pipe drain,test.concurrent) and that the debug-only[macro] callstdout line is handled.
Extended reasoning...
Overview
One-line logic fix in visit_binding_and_expr_for_macro (src/js_parser/visit/mod.rs): the in-place compaction of a macro-returned object's property array is changed from a raw-pointer copy to a safe slice_mut().swap(), and end is only advanced when query.i >= end. A new test file exercises every 3-key permutation, nested/aliased destructuring, and a bun build subset-elimination check.
Correctness
I traced the loop by hand. as_property (src/ast/e.rs:1302) is a linear scan returning the first key match with its index. With copy, props[end] was overwritten before later binding keys could look it up; with swap the displaced entry lands at query.i (in the not-yet-placed tail) and remains findable. The query.i < end case can only occur when the current binding key matches a property already placed — i.e. a repeated key like {a: x, a: y} — and swapping there would move an unmatched tail entry into the kept prefix, so skipping is required. The recursion / const_values.put uses query.expr captured before the swap, so it reads the correct value. Both swap indices are in-bounds (end ≤ matches-so-far ≤ len; query.i < len from as_property), so the removed unsafe is genuinely unnecessary.
Security risks
None. Purely build-time AST bookkeeping on macro-produced object literals; no untrusted-length arithmetic, no allocator changes.
Level of scrutiny
Low-to-medium. ~10 lines of logic in a narrow macro-only path (was_originally_macro gate), matching the existing swap-compact idiom used for decl compaction later in the same file. The fix is mechanically simple and the PR description's mechanism explanation checks out against as_property's implementation.
Other factors
Tests are thorough for the bug class (all permutations, nested, aliased, subset build output + runtime), use test.concurrent, drain pipes concurrently, and account for the debug build's [macro] call stdout via lastLine. The build-output regex is whitespace-tolerant across newlines. robobun confirmed the tests fail on release bun and pass on this branch. No prior reviewer comments to address; no CODEOWNERS on src/js_parser/.
Fixes #9613.
Repro
Cause
visit_binding_and_expr_for_macroiterates the binding pattern's keys, looks each up in the macro-returned object withas_property, and compacts the object's property array to only the referenced entries. The compaction was an in-placeprops[end] = props[query.i]that ran inside the same loop, so when binding key order differed from the object's property order the copy intoprops[end]overwrote an entry a later binding key had not yet looked up. The subsequentas_propertyfor that key then failed and the emitted initializer dropped the property.Fix
Swap
props[end]andprops[query.i]instead of copying, so the displaced entry stays in the array (in the not-yet-placed tail) and remains reachable for later lookups. Skip the swap whenquery.i < end, which only occurs when the binding repeats a key already placed; the property is already in the kept prefix.This matches the existing compaction pattern elsewhere in the visitor (
scan_side_effects.rs,visit/mod.rsdecl compaction) and removes anunsafeblock.Tests
test/bundler/transpiler/macro-destructure.test.tscovers every permutation of a three-key binding, a nested/aliased destructure where the old behaviour threwTypeError: Cannot destructure ... from undefined, and abun buildcheck that the subset dead-code-elimination still keeps only the referenced keys. The first two fail on the released bun and all three pass with this change.The tests are in their own file rather than
macro-test.test.tsbecause that file currently aborts the debug build (!m_topGCOwnedDataScopeinHeap::clearConcurrentRetainedDataIfPossible, tracked separately).Separate from #7196 / #12067 (see #35548), which are about whether a macro result may enter
const_valuesat all; this is the property-matching bookkeeping once it does.