fix(minifier): avoid incorrect logical assignment transformation when base object may be mutated#16802
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. This stack of pull requests is managed by Graphite. Learn more about stacking. |
There was a problem hiding this comment.
Pull request overview
This PR fixes an incorrect minifier transformation where logical expressions with member expressions could be incorrectly transformed to logical assignment operators when the base object is mutated in a sequence expression. The fix adds a check to prevent transforming expressions like x.y || (x = {}, x.y = 3) to x.y ||= (x = {}, 3), which would have different semantics because the logical assignment operator captures x before evaluating the RHS.
- Adds a guard to check if member expression base objects may be mutated before applying the optimization
- Implements helper functions to recursively check if expressions reference mutated symbols
- Adds comprehensive test coverage for various mutation scenarios
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| crates/oxc_minifier/src/peephole/minimize_logical_expression.rs | Adds member_object_may_be_mutated and expression_reference_may_be_mutated helper functions to check if member expression base objects may be mutated, and uses this check to guard the logical assignment optimization for sequence expressions |
| crates/oxc_minifier/src/peephole/minimize_conditions.rs | Adds comprehensive test cases covering various scenarios where the base object is mutated (direct assignment, nested properties, function mutations) and cases where transformation is still safe |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
CodSpeed Performance ReportMerging #16802 will not alter performanceComparing Summary
Footnotes
|
There was a problem hiding this comment.
The new guard correctly addresses the x.y || (x = {}, x.y = 3) mis-optimization, but the safety check may be under-conservative for this roots and is easy to misread due to naming that doesn’t reflect its “object identity stability” intent. Tightening the conservatism for this and documenting the invariant would reduce the chance of future incorrect rewrites. The added regression tests are strong and cover the reported issue well.
Additional notes (2)
- Maintainability |
crates/oxc_minifier/src/peephole/minimize_logical_expression.rs:296-329
The helper nameexpression_reference_may_be_mutatedis doing two different things:
- For identifiers, it checks
symbol_is_mutated()(good and precise). - For all other expressions, it returns
true(unknown), which is broader than “reference may be mutated” (it’s more like “object identity may differ between evaluations”).
This matters because the call site is specifically about capturing the base object for member assignment LHS under ||=/&&=/??=. The current logic is conservative (good for correctness), but the naming/doc doesn’t make that conservatism obvious, and future maintainers may “optimize” it incorrectly.
A short doc comment explaining the intended safety property (object identity stability between LHS evaluation and RHS) would prevent regressions.
- Maintainability |
crates/oxc_minifier/src/peephole/minimize_logical_expression.rs:230-241
The mutation check is only applied to the LHS assignment target (i.e., the member expression being assigned) viamember_object_may_be_mutated(&assignment_expr.left, ctx). That’s aligned with the reported bug, but it leaves a potential blind spot if the logical-expression left operand is a member expression with a different base than the assignment target (even ifhas_no_side_effect_for_evaluation_same_targetpasses due to structural equivalence rules). If that helper allows some equivalence cases where bases are “effectively same target” but can still diverge after mutation (e.g., via aliasing), the guard might be too narrow.
Given this is a correctness fix, it’s worth double-checking the contract of has_no_side_effect_for_evaluation_same_target: does it guarantee the left operand and assignment target share the same base object identity in a way that makes checking only assignment_expr.left sufficient?
Summary of changes
What changed
✅ Minifier correctness guard
- Added a conservative bail-out in
minimize_logical_expression.rsto avoid transforming patterns likex.y || (x = {}, x.y = 3)intox.y ||= (x = {}, 3)when the base object of the member expression may be mutated before the RHS finishes. - Implemented helper checks:
member_object_may_be_mutated()extracts the member-expression object from anAssignmentTarget.expression_reference_may_be_mutated()recursively determines whether the object ultimately refers to a symbol thatsymbol_is_mutated()marks as mutated (or is otherwise conservatively treated as mutable).
🧪 Expanded regression coverage
- Added multiple new tests in
minimize_conditions.rscovering:- Reassignment of the base object in RHS sequences.
- Safe transformations when only the property chain is mutated (not the base binding).
- Conservative no-transform cases when a symbol is mutated anywhere in scope (including inside functions).
- Deep member chains like
x.y.z.w.
Links:
- Regression: #16647
crates/oxc_minifier/src/peephole/minimize_logical_expression.rs
Outdated
Show resolved
Hide resolved
ccd9311 to
464caa7
Compare
crates/oxc_minifier/src/peephole/minimize_logical_expression.rs
Outdated
Show resolved
Hide resolved
crates/oxc_minifier/src/peephole/minimize_logical_expression.rs
Outdated
Show resolved
Hide resolved
80f9338 to
b75cfeb
Compare
Merge activity
|
b75cfeb to
76b1dc7
Compare
…is mutated (#17472) Extends the mutation check from #16802 to `remove_unused_expression.rs`. The transformation `x.y != null || (x = {}, x.y = 3)` → `x.y ??= (x = {}, 3)` changes semantics when `x` is reassigned because `??=` captures the reference before evaluating the RHS. ### Changes - Add `member_object_may_be_mutated()` check before `??=` transformation in nullish coalescing optimization - Transformation now correctly: - Allows `x.y != null || (x = {}, x.y = 3)` → `x.y ?? (x = {}, x.y = 3)` (safe) - Blocks `x.y != null || (x = {}, x.y = 3)` → `x.y ??= (x = {}, 3)` (unsafe) - Permits `x.y != null || (foo(), x.y = 3)` → `x.y ??= (foo(), 3)` when no mutation ### Example ```js var x = {}; x.y != null || (x = {}, x.y = 3) // x becomes { y: 3 } x.y ??= (x = {}, 3) // x becomes {} (incorrect!) x.y ?? (x = {}, x.y = 3) // x becomes { y: 3 } (correct) ``` <!-- START COPILOT CODING AGENT SUFFIX --> <!-- START COPILOT ORIGINAL PROMPT --> <details> <summary>Original prompt</summary> > Add tests for #16802 (comment) </details> <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Fixes #16647
Use
symbol_is_mutated()to check if the member expression base object may be reassigned, preventing incorrect transformations likex.y || (x = {}, x.y = 3)tox.y ||= (x = {}, 3).🤖 generated with help from Claude Opus 4.5