fix(minifier): keep the default in shorthand assignment targets (#28183)

`try_compress_assignment_target_property` rewrites `{ a: a }` to `{ a }`
in assignment patterns. It found the target with
`AssignmentTargetMaybeDefault::identifier_mut()`, which also matches `a:
a = b`, and then built the shorthand without an initializer, so the
default was dropped:

```js
({ x: x = f() } = o);
// before: ({ x } = o);
// after:  ({ x = f() } = o);
```

When `o.x` is `undefined`, `x` stayed `undefined` and `f()` was not
called. The rewrite now moves the initializer into the shorthand. Both
forms name an anonymous function default after the identifier, so
`.name` does not change.

This causes 9 of the 21 `obj-prop-elem-init-*` minify failures in
`runtime.snap` (the `evaluation`, `yield-expr` and `let` cases under
`expressions/assignment/dstr/`, `statements/for-of/dstr/` and
`statements/for-await-of/`). Their minified output now keeps the
default. The other 12 are `fn-name-*` cases with a different cause, and
their output is unchanged. `runtime.snap` is not regenerated here.

Tests cover literal, call, `yield` and `await` defaults, a default that
reads the target, nested and array patterns, `for...in`, `for...of` and
`for await...of` heads, a rest sibling and a computed string key, plus
forms that must stay unchanged. `cargo minsize` and `cargo allocs` show
no change.

AI-assisted (Claude Code).
This commit is contained in:
waltu authored and GitHub committed 2026-10-27 09:40:53 +08:00
1 parent def47b8093
commit 520a26edd6
2 files changed
+37 -5

No files matched your search

@@ -96,7 +96,7 @@ impl<'a> PeepholeOptimizations {
prop: &mut AssignmentTargetProperty<'a>,
ctx: &mut TraverseCtx<'a>,
) {
// `a: a` -> `a`
// `a: a` -> `a`, `a: a = b` -> `a = b`
if let AssignmentTargetProperty::AssignmentTargetPropertyProperty(assign_target_prop_prop) =
prop
{
@@ -105,11 +105,16 @@ impl<'a> PeepholeOptimizations {
return;
};
if prop_name == ident.name {
let ident = ident.take_in(ctx);
// `identifier_mut` also matches `a: a = b`, so keep its default.
let init = match &mut assign_target_prop_prop.binding {
AssignmentTargetMaybeDefault::AssignmentTargetWithDefault(target) => {
Some(target.init.take_in(ctx))
}
_ => None,
};
let new_prop = AssignmentTargetProperty::new_assignment_target_property_identifier(
ident.span,
ident.take_in(ctx),
None,
ctx,
ident.span, ident, init, ctx,
);
ctx.replace_assignment_target_property(prop, new_prop);
}
@@ -939,6 +939,33 @@ fn test_compress_destructuring_assignment_target() {
test("var {y: y} = x", "var {y} = x");
test("var {y: z, 'z': y} = x", "var {y: z, z: y} = x");
test("var {y: y, 'z': z} = x", "var {y, z} = x");
test("var {y: y = f()} = x", "var {y = f()} = x");
test("({ y: y } = x)", "({ y } = x)");
test("({ 'y': y } = x)", "({ y } = x)");
test("({ y: y = 1 } = x)", "({ y = 1 } = x)");
test("({ y: y = f() } = x)", "({ y = f() } = x)");
test("({ y: y = y } = x)", "({ y = y } = x)");
test("({ y: y = function() {} } = x)", "({ y = function() {} } = x)");
test("({ y: y = f(), z: z } = x)", "({ y = f(), z } = x)");
test("({ a: { y: y = f() } } = x)", "({ a: { y = f() } } = x)");
test("[{ y: y = f() }] = x", "[{ y = f() }] = x");
test("for ({ y: y = f() } of x);", "for ({ y = f() } of x);");
test("for ({ y: y = f() } in x);", "for ({ y = f() } in x);");
test(
"async function g() { for await ({ y: y = f() } of x); }",
"async function g() { for await ({ y = f() } of x); }",
);
test("function* g() { ({ y: y = yield } = x) }", "function* g() { ({ y = yield } = x) }");
test(
"async function g() { ({ y: y = await f() } = x) }",
"async function g() { ({ y = await f() } = x) }",
);
test("({ y: y = 1, ...z } = x)", "({ y = 1, ...z } = x)");
test("({ ['y']: y = 1 } = x)", "({ y = 1 } = x)");
test_same("({ [y]: y = 1 } = x)");
test_same("({ y: z = f() } = x)");
test_same("({ y: a.y = f() } = x)");
}
#[test]