bug(minifier): @__PURE__ annotated initializers of unused declarations not fully removed
Description
When an unused variable declaration has a @__PURE__-annotated call expression as its initializer (e.g., a pure IIFE), the minifier incorrectly preserves the initializer body as a side-effectful expression statement instead of removing it entirely.
Reproduction
Input:
var x = /* @__PURE__ */ (() => {
return oprt(1);
})();
Actual output (incorrect):
Expected output:
// (empty — fully removed)
Oxc Playground: Oxc Playground
Root Cause
In handle_variable_declaration (crates/oxc_minifier/src/peephole/minimize_statements.rs), when an unused declarator is removed, its initializer was checked via init.may_have_side_effects(ctx) to decide whether to keep it as an expression statement. This missed the deeper expression-level optimizations that remove_unused_expression provides — such as handling pure IIFEs, stripping pure calls while preserving their side-effectful arguments.
The issue surfaces when:
- The initializer is a
@__PURE__ IIFE like /* @__PURE__ */ (() => { return oprt(1) })()
may_have_side_effects correctly returns false for the outer call...
- ...BUT during the iterative peephole process, the IIFE body gets inlined first (stripping the pure annotation), turning it into just
oprt(1) which IS side-effectful
Fix
Fixed in PR:
#22251
Replace init.may_have_side_effects(ctx) with !Self::remove_unused_expression(&mut init, ctx) in handle_variable_declaration. This delegates to the full expression removal pipeline that properly handles:
@__PURE__ annotated calls → fully removed
@__PURE__ annotated IIFEs → fully removed
- Pure calls with side-effectful arguments → arguments preserved, call removed
@__NO_SIDE_EFFECTS__ functions → calls removed
- Regular side-effectful expressions → preserved (no behavior change)
Impact on Downstream (Rolldown)
This bug was discovered while investigating a tree-shaking issue in Rolldown. Rolldown uses the Oxc compressor as a pre-processing step before its own tree-shaking pass. When the compressor incorrectly preserves oprt(1) as a top-level statement, Rolldown's AST scanner treats it as side-effectful, causing entire modules and their dependencies to be included in the final bundle even when unused.
bug(minifier):
@__PURE__annotated initializers of unused declarations not fully removedDescription
When an unused variable declaration has a
@__PURE__-annotated call expression as its initializer (e.g., a pure IIFE), the minifier incorrectly preserves the initializer body as a side-effectful expression statement instead of removing it entirely.Reproduction
Input:
Actual output (incorrect):
Expected output:
// (empty — fully removed)Oxc Playground: Oxc Playground
Root Cause
In
handle_variable_declaration(crates/oxc_minifier/src/peephole/minimize_statements.rs), when an unused declarator is removed, its initializer was checked viainit.may_have_side_effects(ctx)to decide whether to keep it as an expression statement. This missed the deeper expression-level optimizations thatremove_unused_expressionprovides — such as handling pure IIFEs, stripping pure calls while preserving their side-effectful arguments.The issue surfaces when:
@__PURE__IIFE like/* @__PURE__ */ (() => { return oprt(1) })()may_have_side_effectscorrectly returnsfalsefor the outer call...oprt(1)which IS side-effectfulFix
Fixed in PR:
#22251
Replace
init.may_have_side_effects(ctx)with!Self::remove_unused_expression(&mut init, ctx)inhandle_variable_declaration. This delegates to the full expression removal pipeline that properly handles:@__PURE__annotated calls → fully removed@__PURE__annotated IIFEs → fully removed@__NO_SIDE_EFFECTS__functions → calls removedImpact on Downstream (Rolldown)
This bug was discovered while investigating a tree-shaking issue in Rolldown. Rolldown uses the Oxc compressor as a pre-processing step before its own tree-shaking pass. When the compressor incorrectly preserves
oprt(1)as a top-level statement, Rolldown's AST scanner treats it as side-effectful, causing entire modules and their dependencies to be included in the final bundle even when unused.