mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
fix(minifier): drop unused-var-init pure IIFEs and preserve annotation for downstream (#22349)
Closes #17480. Supersedes #21526 (gthb) and #22251 (mo-n) — both target the same bug from different angles. This PR takes a single coordinated approach. Closes: #21526 Closes: #22251 ## What A `/* @__PURE__ */`-annotated IIFE assigned to an unused `var`/`let`/`const` was not being dropped, and inlining such an IIFE silently destroyed the annotation that rolldown's tree-shaker relies on. The fix lands three coordinated checks in `substitute_iife_call`, one small swap in `handle_variable_declaration`, and an export-ancestor guard. ### 1. Widened `is_expression_result_unused` `is_expression_result_unused` already short-circuited pure IIFEs to `void 0` for bare expression statements. This PR extends it to also recognize `VariableDeclaratorInit` whose binding has no references and isn't exported. The unused declaration then drops in a single iteration — regardless of body shape (call, new, member access, tagged template, chain, sequence, conditional, etc.). Guards: - `can_remove_unused_declarators` — respects script-mode and direct-eval gates. - `decl.kind().is_using()` — `using` / `await using` run `[Symbol.dispose]` at scope exit, so the init isn't truly unused. - `var_declaration_is_exported` — exported bindings bypass `handle_variable_declaration`, so dropping the init would silently break the export's runtime value. The check looks only at `ctx.ancestors().nth(2)` (the slot directly above `VariableDeclaration`); walking the full chain would over-broaden the guard to function-local vars inside exported functions. ### 2. `iife_inline_would_lose_pure` (DCE-only gate) In DCE-only mode (rolldown's per-module preprocess), refuse to inline a pure IIFE whose body has *any* side effects. The original outer IIFE call has zero arguments, so its `pure` flag covers the body's evaluation as a unit; inlining surfaces sub-effects at the outer call's argument level, where rolldown's side-effect detector flags them regardless of any propagated `pure` flag. Gated on `ctx.state.dce` because in full-minify mode there's no downstream consumer to preserve the annotation for — inline aggressively for smaller output. ### 3. `try_take_iife_body` helper Unifies the three IIFE inline branches (expression body, expression-statement body, return-statement body). Returns `Some(inlined)` after taking the body and stamping `pure = true` on a `CallExpression`/`NewExpression` replacement; returns `None` to bail when `iife_inline_would_lose_pure` says inlining would weaken the assertion — caller leaves the IIFE intact. ### 4. `remove_unused_expression` swap in `handle_variable_declaration` Replace `init.may_have_side_effects(ctx)` with `!Self::remove_unused_expression(&mut init, ctx)`. The smart expression cleanup peels pure-call wrappers and keeps only side-effectful args (`var x = /* @__PURE__ */ foo(a)` → `a;`). ## Relation to #21526 and #22251 | Mechanism | This PR | #21526 (gthb) | #22251 (mo-n) | |---|---|---|---| | Widened unused-var-init drop | ✓ | ✗ | ✓ (as `is_in_unused_variable_declarator`) | | Export-ancestor guard | ✓ | ✗ | ✗ (caught here: exports could be wrongly dropped) | | `remove_unused_expression` swap in `handle_variable_declaration` | ✓ | ✗ | ✓ | | Propagate `pure` onto inlined call/new | ✓ | ✓ (recursive into containers) | ✗ | | Preserve IIFE wrapper when body has any side effect (DCE-only) | ✓ | ✗ | ✗ | | Recursive propagation into sequence/conditional/etc bodies | ✗ | ✓ | ✗ | | TS wrapper transparency in `may_have_side_effects` | ✗ | ✓ | ✗ | Two mechanisms from #21526 are intentionally omitted here: - **Recursive container propagation.** The DCE-only gate preserves the IIFE wrapper for those body shapes, so the outer call's `pure` flag stays visible to downstream tools. Recursive propagation also spreads any developer-lie annotation to inner sub-expressions, which this PR's restraint avoids. - **TS wrapper transparency in `may_have_side_effects`.** Orthogonal; can land separately. If this PR is accepted, #21526 and #22251 should be closed as superseded. ## Coverage | Case | Output | |---|---| | `var x = /* @__PURE__ */ (() => stuff())()` (x unused — original bug) | empty | | `var x = /* @__PURE__ */ (() => g.x)()` (x unused, non-call body) | empty | | `var x = /* @__PURE__ */ foo(a)` (x unused, side-effectful arg) | `a;` | | `var a = /* @__PURE__ */ (() => x)(); console.log(a)` (armano2 #1, full mode) | `var a = x; console.log(a);` | | `(/* @__PURE__ */ (() => !0)() ? () => x() : () => {})()` (armano2 #2) | `x();` | | `export const x = /* @__PURE__ */ (() => _M.exec)();` (lights0123, DCE mode) | preserves IIFE → rolldown can tree-shake | | `export const x = /* @__PURE__ */ (() => foo(bar()))();` (DCE mode, sub-effects) | preserves IIFE → rolldown can tree-shake | | `export function f() { var x = /* @__PURE__ */ (() => foo())(); } f();` | `export function f() {} f();` (function-local var is local, not export) | | `var x = (async () => {})()` / `var x = (function* () {})()` (x unused) | empty (empty-IIFE branch via widened predicate) | ## End-to-end verification with rolldown Locally patched rolldown to use this branch. Added a regression fixture (`tree_shaking/pure_iife_unused_export`) covering five body shapes. **Before this PR** (rolldown bundle for unused-export module): ```js //#region lib.js function maybeExpensive() { ... } function MaybeExpensiveCtor() { ... } const _M = { exec: 1, tag: () => 1 }; maybeExpensive(); new MaybeExpensiveCtor(); _M.exec; _M?.exec; _M.tag`tpl`; //#endregion //#region main.js console.log("hello"); //#endregion ``` **After this PR**: ```js //#region main.js console.log("hello"); //#endregion ``` Two pre-existing rolldown tests (`strict_execution_order/issue_5303`, `concatenate_wrapped_modules`) fail with the local patch — they use `/* #__PURE__ */ (() => globalValue)()` where `globalValue` is set by a sibling module's side effect. The annotation is technically incorrect; the tests previously "worked" only because oxc destroyed the annotation and rolldown defensively wrapped the module. These need a follow-up in rolldown to correct the annotations. ## Snapshots - `minsize.snap` — unchanged. - `allocs_minifier.snap` — `cal.com.tsx` +1 alloc (negligible); `antd.js` +1921 allocs (~0.6% of the 331k baseline). Comes from the new `may_have_side_effects` call in `iife_inline_would_lose_pure` plus the deeper rewrites in `remove_unused_expression`. Trade is correctness/tree-shaking-fidelity for a small intra-pass alloc bump; output size unchanged.
This commit is contained in:
1 parent
70a1e2a81e
commit
5ac7e79672
8 files changed
+263
-28
No files matched your search
@@ -450,8 +450,10 @@ impl<'a> PeepholeOptimizations {
|
||||
for mut decl in declarations {
|
||||
if Self::should_remove_unused_declarator(&decl, ctx) {
|
||||
ctx.state.changed = true;
|
||||
if let Some(init) = decl.init.take()
|
||||
&& init.may_have_side_effects(ctx)
|
||||
// `init` is `mut` because `remove_unused_expression` rewrites
|
||||
// it in place (peeling pure-call wrappers, etc).
|
||||
if let Some(mut init) = decl.init.take()
|
||||
&& !Self::remove_unused_expression(&mut init, ctx)
|
||||
{
|
||||
result.push(ctx.ast.statement_expression(init.span(), init));
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ use oxc_ast::ast::*;
|
||||
use oxc_ecmascript::constant_evaluation::{DetermineValueType, ValueType};
|
||||
|
||||
impl<'a> PeepholeOptimizations {
|
||||
fn can_remove_unused_declarators(ctx: &TraverseCtx<'a>) -> bool {
|
||||
pub(super) fn can_remove_unused_declarators(ctx: &TraverseCtx<'a>) -> bool {
|
||||
ctx.state.options.unused != CompressOptionsUnused::Keep
|
||||
&& !Self::keep_top_level_var_in_script_mode(ctx)
|
||||
&& !ctx.scoping().root_scope_flags().contains_direct_eval()
|
||||
|
||||
@@ -1709,9 +1709,51 @@ impl<'a> PeepholeOptimizations {
|
||||
)
|
||||
}
|
||||
|
||||
/// Checks if the expression result is unused (i.e., in an expression statement context).
|
||||
/// Whether the expression's result will be discarded — bare expression
|
||||
/// statement, or the init of a `var`/`let`/`const` whose binding has no
|
||||
/// references and isn't exported. Used by the IIFE inliner to short-circuit
|
||||
/// pure-annotated IIFEs to `void 0` so they drop regardless of body shape,
|
||||
/// and to allow `(async () => {})()` / `(function* () {})()` (whose return
|
||||
/// value isn't a meaningful result) to collapse in those positions too.
|
||||
/// Fixes <https://github.com/oxc-project/oxc/issues/17480>.
|
||||
fn is_expression_result_unused(ctx: &TraverseCtx<'a>) -> bool {
|
||||
matches!(ctx.parent(), Ancestor::ExpressionStatementExpression(_))
|
||||
match ctx.parent() {
|
||||
Ancestor::ExpressionStatementExpression(_) => true,
|
||||
Ancestor::VariableDeclaratorInit(decl) => {
|
||||
if !Self::can_remove_unused_declarators(ctx) {
|
||||
return false;
|
||||
}
|
||||
// `using` runs `[Symbol.dispose]` at scope exit.
|
||||
if decl.kind().is_using() {
|
||||
return false;
|
||||
}
|
||||
let BindingPattern::BindingIdentifier(ident) = decl.id() else {
|
||||
return false;
|
||||
};
|
||||
if !ctx.scoping().symbol_is_unused(ident.symbol_id()) {
|
||||
return false;
|
||||
}
|
||||
!Self::var_declaration_is_exported(ctx)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// `true` if the `VariableDeclaration` that contains the current expression
|
||||
/// (entered via `VariableDeclaratorInit`) sits directly under an `export`
|
||||
/// wrapper. Exports are cross-module reachable, and the inner
|
||||
/// `VariableDeclaration` never routes through `handle_variable_declaration`
|
||||
/// — dropping its init would silently break the export's runtime value.
|
||||
///
|
||||
/// Checks the exact ancestor slot above `VariableDeclaration` only;
|
||||
/// walking the full chain would over-broaden the guard to function-local
|
||||
/// vars inside exported functions.
|
||||
fn var_declaration_is_exported(ctx: &TraverseCtx<'a>) -> bool {
|
||||
// Only `ExportNamedDeclaration`'s `declaration` field can hold a
|
||||
// `VariableDeclaration`. `export default` wraps a function / class /
|
||||
// expression — never a `VariableDeclaration` — so no arm is needed
|
||||
// for it.
|
||||
matches!(ctx.ancestors().nth(2), Some(Ancestor::ExportNamedDeclarationDeclaration(_)))
|
||||
}
|
||||
|
||||
/// Optimizes the usage of Immediately Invoked Function Expressions (IIFEs)
|
||||
@@ -1753,49 +1795,59 @@ impl<'a> PeepholeOptimizations {
|
||||
return;
|
||||
}
|
||||
|
||||
let is_pure =
|
||||
(call_expr.pure && ctx.annotations()) || ctx.manual_pure_functions(&call_expr.callee);
|
||||
// The callee here is always a function/arrow literal, so
|
||||
// `manual_pure_functions` (which matches named paths like `styled`)
|
||||
// can never apply — only an explicit `/* @__PURE__ */` annotation can.
|
||||
let is_pure = call_expr.pure && ctx.annotations();
|
||||
|
||||
if let Expression::ArrowFunctionExpression(f) = &mut call_expr.callee
|
||||
&& !f.r#async
|
||||
&& !f.params.has_parameter()
|
||||
&& f.body.statements.len() == 1
|
||||
{
|
||||
if f.expression {
|
||||
if let Some(expr) = f.get_expression_mut() {
|
||||
// Replace "(() => foo())()" with "foo()"
|
||||
let expr = f.get_expression_mut().unwrap();
|
||||
if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
*e = ctx.ast.void_0(call_expr.span);
|
||||
*e = if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
ctx.ast.void_0(call_expr.span)
|
||||
} else if let Some(taken) = Self::try_take_iife_body(expr, is_pure, ctx) {
|
||||
taken
|
||||
} else {
|
||||
*e = expr.take_in(ctx.ast);
|
||||
}
|
||||
return;
|
||||
};
|
||||
ctx.state.changed = true;
|
||||
return;
|
||||
}
|
||||
match &mut f.body.statements[0] {
|
||||
Statement::ExpressionStatement(expr_stmt) => {
|
||||
// Replace "(() => { foo() })()" with "(foo(), undefined)"
|
||||
if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
*e = ctx.ast.void_0(call_expr.span);
|
||||
} else {
|
||||
*e = ctx.ast.expression_sequence(expr_stmt.span, {
|
||||
*e = if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
ctx.ast.void_0(call_expr.span)
|
||||
} else if let Some(taken) =
|
||||
Self::try_take_iife_body(&mut expr_stmt.expression, is_pure, ctx)
|
||||
{
|
||||
ctx.ast.expression_sequence(expr_stmt.span, {
|
||||
let mut sequence = ctx.ast.vec();
|
||||
sequence.push(expr_stmt.expression.take_in(ctx.ast));
|
||||
sequence.push(taken);
|
||||
sequence.push(ctx.ast.void_0(call_expr.span));
|
||||
sequence
|
||||
});
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
ctx.state.changed = true;
|
||||
}
|
||||
Statement::ReturnStatement(ret_stmt) => {
|
||||
if let Some(argument) = &mut ret_stmt.argument {
|
||||
// Replace "(() => { return foo() })()" with "foo()"
|
||||
if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
*e = ctx.ast.void_0(call_expr.span);
|
||||
*e = if is_pure && Self::is_expression_result_unused(ctx) {
|
||||
ctx.ast.void_0(call_expr.span)
|
||||
} else if let Some(taken) = Self::try_take_iife_body(argument, is_pure, ctx)
|
||||
{
|
||||
taken
|
||||
} else {
|
||||
*e = argument.take_in(ctx.ast);
|
||||
}
|
||||
return;
|
||||
};
|
||||
ctx.state.changed = true;
|
||||
}
|
||||
}
|
||||
@@ -1803,6 +1855,48 @@ impl<'a> PeepholeOptimizations {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the IIFE body out for inlining, propagating the outer `pure`
|
||||
/// annotation onto a call/new body. Propagation runs in both DCE and
|
||||
/// full-minify modes; only the bail-out (via
|
||||
/// [`Self::iife_inline_would_lose_pure`]) is gated to DCE mode, where
|
||||
/// preserving the IIFE wrapper matters for downstream tools.
|
||||
/// Returns `None` to signal the caller should leave the IIFE intact.
|
||||
fn try_take_iife_body(
|
||||
body: &mut Expression<'a>,
|
||||
is_pure: bool,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> Option<Expression<'a>> {
|
||||
if Self::iife_inline_would_lose_pure(is_pure, body, ctx) {
|
||||
return None;
|
||||
}
|
||||
let mut taken = body.take_in(ctx.ast);
|
||||
if is_pure {
|
||||
match &mut taken {
|
||||
Expression::CallExpression(c) => c.pure = true,
|
||||
Expression::NewExpression(n) => n.pure = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Some(taken)
|
||||
}
|
||||
|
||||
/// Whether inlining the IIFE body would weaken the outer pure assertion.
|
||||
/// Fires only in DCE-only mode (rolldown's per-module preprocess): the
|
||||
/// outer call has no arguments, so its `pure` flag covers the entire
|
||||
/// body. Inlining any side-effectful body surfaces sub-effects at the
|
||||
/// outer call site, where rolldown's side-effect detector treats them
|
||||
/// as real side effects regardless of the inlined node's `pure` flag.
|
||||
fn iife_inline_would_lose_pure(
|
||||
is_pure: bool,
|
||||
body: &Expression<'a>,
|
||||
ctx: &TraverseCtx<'a>,
|
||||
) -> bool {
|
||||
if !is_pure || !ctx.state.dce {
|
||||
return false;
|
||||
}
|
||||
body.may_have_side_effects(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
struct ClassPropertyKeyParent {
|
||||
|
||||
@@ -57,6 +57,16 @@ pub(crate) fn test_same_options(source_text: &str, options: &CompressOptions) {
|
||||
test_options(source_text, source_text, options);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn test_smallest(source_text: &str, expected: &str) {
|
||||
test_options(source_text, expected, &CompressOptions::smallest());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn test_same_smallest(source_text: &str) {
|
||||
test_smallest(source_text, source_text);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub(crate) fn test_options_source_type(
|
||||
source_text: &str,
|
||||
|
||||
@@ -546,3 +546,32 @@ fn remove_pure_function_calls() {
|
||||
test("var foo = function() {}; foo()", "");
|
||||
test_same("function foo() { bar() } foo()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserve_pure_iife_in_used_position_for_downstream_treeshake() {
|
||||
// https://github.com/oxc-project/oxc/issues/17480
|
||||
//
|
||||
// In DCE-only mode the IIFE inliner refuses to inline a
|
||||
// `@__PURE__`-annotated IIFE when the body can't carry a `pure` flag,
|
||||
// so the outer call's annotation stays visible to rolldown's
|
||||
// side-effect detector.
|
||||
|
||||
test_same("export const exec = /* @__PURE__ */ (() => _M.exec)();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => foo()?.bar())();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => foo`tpl`)();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => (a(), b()))();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => c ? a() : b())();");
|
||||
// Bare-identifier conditional — unknown global reads count as side
|
||||
// effects in DCE mode, so the IIFE wrapper stays.
|
||||
test_same("export const x = /* @__PURE__ */ (() => a ? b : c)();");
|
||||
|
||||
test_same("export const x = /* @__PURE__ */ (() => foo())();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => new Foo())();");
|
||||
// Effectful arg inside the body call — the original IIFE could be dropped
|
||||
// wholesale; the inlined form would expose `bar()` as a surviving side
|
||||
// effect of the outer call.
|
||||
test_same("export const x = /* @__PURE__ */ (() => foo(bar()))();");
|
||||
test_same("export const x = /* @__PURE__ */ (() => new Foo(bar()))();");
|
||||
|
||||
test("export const x = /* @__PURE__ */ (() => 42)();", "export const x = 42;");
|
||||
}
|
||||
@@ -2,7 +2,7 @@ use oxc_span::SourceType;
|
||||
|
||||
use crate::{
|
||||
CompressOptions, TreeShakeOptions, test_options, test_options_source_type, test_same_options,
|
||||
test_same_options_source_type,
|
||||
test_same_options_source_type, test_same_smallest, test_smallest,
|
||||
};
|
||||
|
||||
#[test]
|
||||
@@ -56,6 +56,106 @@ fn remove_unused_variable_declaration() {
|
||||
test_same_options("for (var x = foo; ; );", &options); // can be improved
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_unused_pure_iife_init() {
|
||||
// https://github.com/oxc-project/oxc/issues/17480
|
||||
test_smallest("var x = /* @__PURE__ */ foo()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ new Foo()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ foo(a)", "a;");
|
||||
test_smallest("var x = /* @__PURE__ */ foo(bar())", "bar();");
|
||||
test_smallest("var x = /* @__PURE__ */ new Foo(bar())", "bar();");
|
||||
test_smallest("var x = /* @__PURE__ */ foo(/* @__PURE__ */ bar(z))", "z;");
|
||||
|
||||
test_smallest("var x = /* @__PURE__ */ (() => foo())()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => new Foo())()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => { return foo() })()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => { foo() })()", "");
|
||||
|
||||
test_smallest("var x = /* @__PURE__ */ (() => g.x)()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => g[k])()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => foo`tpl`)()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => [a, b])()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => ({ a }))()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => a + b)()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => `${a}`)()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => foo()?.bar())()", "");
|
||||
test_smallest("var x = /* @__PURE__ */ (() => a ? b : c)()", "");
|
||||
|
||||
test_smallest("var x = /* @__PURE__ */ (function() { return foo() })()", "");
|
||||
|
||||
test_smallest("let x = /* @__PURE__ */ (() => g.x)()", "");
|
||||
test_smallest("const x = /* @__PURE__ */ (() => g.x)()", "");
|
||||
|
||||
test_smallest("var x = /* @__PURE__ */ foo(), y = bar(); use(y);", "var y = bar(); use(y);");
|
||||
|
||||
// Referenced bindings keep the declarator — `symbol_is_unused` blocks
|
||||
// the drop. Propagation still inlines the IIFE body.
|
||||
test_same_smallest("var x = /* @__PURE__ */ foo(); use(x);");
|
||||
test_smallest(
|
||||
"var x = /* @__PURE__ */ (() => foo())(); use(x);",
|
||||
"var x = /* @__PURE__ */ foo(); use(x);",
|
||||
);
|
||||
test_smallest("var x = /* @__PURE__ */ (() => g.x)(); use(x);", "var x = g.x; use(x);");
|
||||
test_smallest(
|
||||
"var x = /* @__PURE__ */ (() => { return foo() })(); use(x);",
|
||||
"var x = /* @__PURE__ */ foo(); use(x);",
|
||||
);
|
||||
// Conditional body — propagation only fires on Call/New, so the
|
||||
// top-level conditional is inlined without an annotation.
|
||||
test_smallest(
|
||||
"var x = /* @__PURE__ */ (() => a ? b : c)(); use(x);",
|
||||
"var x = a ? b : c; use(x);",
|
||||
);
|
||||
|
||||
// Exported bindings are cross-module reachable — the export-ancestor
|
||||
// check blocks the early drop.
|
||||
test_smallest("export var x = /* @__PURE__ */ foo()", "export var x = /* @__PURE__ */ foo();");
|
||||
test_smallest(
|
||||
"export const x = /* @__PURE__ */ (() => foo())();",
|
||||
"export const x = /* @__PURE__ */ foo();",
|
||||
);
|
||||
test_smallest("export const x = /* @__PURE__ */ (() => g.x)();", "export const x = g.x;");
|
||||
test_same_smallest("var x = /* @__PURE__ */ foo(); export { x }");
|
||||
|
||||
test_smallest("var x = (() => g.x)();", "g.x;");
|
||||
|
||||
// `using` runs `[Symbol.dispose]` at scope exit, so the declarator stays.
|
||||
test_smallest("using x = /* @__PURE__ */ (() => foo())()", "using x = /* @__PURE__ */ foo();");
|
||||
test_smallest(
|
||||
"await using x = /* @__PURE__ */ (() => foo())()",
|
||||
"await using x = /* @__PURE__ */ foo();",
|
||||
);
|
||||
|
||||
// Function-local var inside an exported function — the export ancestor
|
||||
// walk must not be fooled by `f` being exported. `x` is a local.
|
||||
test_smallest(
|
||||
"export function f() { var x = /* @__PURE__ */ (() => foo())(); } f();",
|
||||
"export function f() {} f();",
|
||||
);
|
||||
|
||||
// Empty async/generator IIFE in unused-var-init position now collapses
|
||||
// through `is_expression_result_unused` (which the widening newly covers).
|
||||
test_smallest("var x = (async () => {})()", "");
|
||||
test_smallest("var x = (function* () {})()", "");
|
||||
|
||||
// `can_remove_unused_declarators` blocks top-level `var` drops in script
|
||||
// mode (the binding is an observable global). The IIFE inlines with
|
||||
// propagation as in any other position, but the declarator stays.
|
||||
test_options_source_type(
|
||||
"var x = /* @__PURE__ */ (() => stuff())()",
|
||||
"var x = /* @__PURE__ */ stuff();",
|
||||
SourceType::cjs().with_script(true),
|
||||
&CompressOptions::smallest(),
|
||||
);
|
||||
|
||||
// Direct eval at the root scope blocks the drop — eval might reference
|
||||
// the binding even when static analysis sees no use.
|
||||
test_smallest(
|
||||
"eval('x'); var x = /* @__PURE__ */ (() => stuff())()",
|
||||
"eval('x'); var x = /* @__PURE__ */ stuff();",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_unused_function_declaration() {
|
||||
let options = CompressOptions::smallest();
|
||||
|
||||
@@ -426,7 +426,7 @@ fn test_fold_iife() {
|
||||
);
|
||||
test(
|
||||
"function foo(x) { if (x) { return /* @__PURE__ */ (() => bar())() } return x }",
|
||||
"function foo(x) { return x && bar() }",
|
||||
"function foo(x) { return x && /* @__PURE__ */ bar() }",
|
||||
);
|
||||
test(
|
||||
"function foo(x) { if (x) { return /* @__PURE__ */ (() => { return 42 })() } return x }",
|
||||
|
||||
@@ -2,13 +2,13 @@ File | File size || Sys allocs | Sys reallocs |
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
checker.ts | 2.92 MB || 286 | 37 || 152659 | 28244
|
||||
|
||||
cal.com.tsx | 1.06 MB || 16324 | 46 || 37153 | 4570
|
||||
cal.com.tsx | 1.06 MB || 16324 | 46 || 37154 | 4570
|
||||
|
||||
RadixUIAdoptionSection.jsx | 2.52 kB || 29 | 6 || 30 | 6
|
||||
|
||||
pdf.mjs | 567.30 kB || 4052 | 417 || 47472 | 7746
|
||||
|
||||
antd.js | 6.69 MB || 7702 | 1657 || 331684 | 69349
|
||||
antd.js | 6.69 MB || 7702 | 1657 || 333605 | 69349
|
||||
|
||||
binder.ts | 193.08 kB || 66 | 23 || 7075 | 824
|
||||
|
||||
Reference in new issue
Block a user