mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(minifier): normalize { x: x } shorthand so adjacent-if merge is idempotent (#22401)
## Why
The merge pass for consecutive `if (..) return X;` statements decides whether two jump bodies are equal via `content_eq`, and `ObjectProperty::content_eq` includes the `shorthand` flag. Codegen, in contrast, always prints `{ x: x }` as `{ x }` whenever the value is an identifier matching the key. The flag disagreement made the merge non-idempotent. Given:
```js
function _(body) {
if (a) return { body };
if (b) return { body };
if (c) return { body };
if (d) return { body: body };
}
```
pass 1 merged only the first three (their `shorthand` matched) and left the trailing if alone:
```js
function _(body) {
if (a || b || c) return { body };
if (d) return { body };
}
```
pass 2 then merged all four, because the printed `{ body }` reparses with `shorthand = true`. The fixed-point loop converges, but a second call to `Compressor::build` would still mutate the AST — the contract `monitor-oxc` checks.
## Fix
Extend `substitute_object_property` (already called once per `ObjectProperty` exit in the peephole loop, so no new traversal dispatch) to set `shorthand = true` whenever the value is an identifier with the same name as a non-computed, non-method, non-`__proto__` init key. The AST now matches what codegen would emit, so subsequent `content_eq`-based merges converge in a single pass. Checks are ordered to bail on the common case first (already shorthand, or non-identifier value); output text and the `minsize` / `allocs` snapshots are unchanged.
Originally surfaced by `monitor-oxc` CI on `@typespec/ts-http-runtime`'s `sendRequest.js`: https://github.com/oxc-project/monitor-oxc/actions/runs/25841541741/job/75927903765
This commit is contained in:
1 parent
dcc6d38330
commit
c27a8cfa4c
2 files changed
+75
-1
No files matched your search
@@ -37,7 +37,38 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
}
|
||||
|
||||
// Normalise the key before checking shorthand: `try_compress_property_key`
|
||||
// can turn `{ "x": x }` into `{ x: x }`, which then becomes a candidate
|
||||
// for shorthand normalisation in the same visit.
|
||||
Self::try_compress_property_key(&mut prop.key, &mut prop.computed, ctx);
|
||||
Self::normalize_object_property_shorthand(prop);
|
||||
}
|
||||
|
||||
/// `{ x: x }` is observationally equivalent to `{ x }`, and codegen always
|
||||
/// prints it as `{ x }`. Set `shorthand = true` so the AST matches the
|
||||
/// printed output; otherwise content-equality checks (e.g. when merging
|
||||
/// adjacent `if` statements with identical jump bodies) treat the two
|
||||
/// forms as different on the first pass and only converge on the second.
|
||||
///
|
||||
/// Output text is unchanged, so we deliberately do not flip
|
||||
/// `ctx.state.changed`.
|
||||
fn normalize_object_property_shorthand(prop: &mut ObjectProperty<'a>) {
|
||||
if prop.shorthand {
|
||||
return;
|
||||
}
|
||||
let Expression::Identifier(value) = &prop.value else { return };
|
||||
if prop.computed || prop.method || prop.kind != PropertyKind::Init {
|
||||
return;
|
||||
}
|
||||
let PropertyKey::StaticIdentifier(key) = &prop.key else { return };
|
||||
// `{ __proto__: __proto__ }` triggers the Annex B.3.1 proto setter
|
||||
// (literal `__proto__` key, non-computed, non-shorthand, non-method),
|
||||
// but `{ __proto__ }` is a plain shorthand `IdentifierReference` that
|
||||
// creates a regular own data property and does NOT set `[[Prototype]]`.
|
||||
// Converting would change observable behaviour, so bail out.
|
||||
if key.name == value.name && key.name != "__proto__" {
|
||||
prop.shorthand = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn substitute_assignment_target_property_property(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::test;
|
||||
use crate::{test, test_same};
|
||||
|
||||
#[test]
|
||||
fn test_for_variable_declaration() {
|
||||
@@ -80,3 +80,46 @@ fn test_max_conditional_depth_caps_return_ternary_chain() {
|
||||
|
||||
test(&input, &output);
|
||||
}
|
||||
|
||||
// https://github.com/oxc-project/monitor-oxc/actions/runs/25841541741/job/75927903765
|
||||
// `{ body }` and `{ body: body }` are observationally equivalent, so adjacent
|
||||
// jump statements returning either form should collapse into a single `if`.
|
||||
// The bug was that ObjectProperty's structural equality compares the `shorthand`
|
||||
// flag, leaving the trailing `{ body: body }` outside the merged chain on the
|
||||
// first pass.
|
||||
#[test]
|
||||
fn test_merge_adjacent_ifs_with_shorthand_object_property() {
|
||||
test(
|
||||
"function _(body) {
|
||||
if (a) return { body };
|
||||
if (b) return { body };
|
||||
if (c) return { body };
|
||||
if (d) return { body: body };
|
||||
}",
|
||||
"function _(body) { if (a || b || c || d) return { body }; }",
|
||||
);
|
||||
// String-literal key normalises to identifier first, then to shorthand —
|
||||
// both transforms must land in a single Compressor::build call.
|
||||
test(
|
||||
"function _(body) {
|
||||
if (a) return { body };
|
||||
if (b) return { 'body': body };
|
||||
}",
|
||||
"function _(body) { if (a || b) return { body }; }",
|
||||
);
|
||||
test_same(
|
||||
"function _(body, other) {
|
||||
if (a) return { body };
|
||||
if (b) return { other };
|
||||
}",
|
||||
);
|
||||
}
|
||||
|
||||
// `{ __proto__: __proto__ }` sets `[[Prototype]]` via the Annex B.3.1 proto
|
||||
// setter, while `{ __proto__ }` is a plain shorthand that creates a regular
|
||||
// own data property. Normalising the former into the latter would change
|
||||
// observable behaviour, so it must be left alone.
|
||||
#[test]
|
||||
fn test_object_property_shorthand_normalisation_skips_proto_setter() {
|
||||
test_same("function _(__proto__) { return { __proto__: __proto__ }; }");
|
||||
}
|
||||
Reference in new issue
Block a user