Skip to content

bug(minifier): @__PURE__ annotated initializers of unused declarations not fully removed #22255

@mo-n

Description

@mo-n

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):

oprt(1);

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:

  1. The initializer is a @__PURE__ IIFE like /* @__PURE__ */ (() => { return oprt(1) })()
  2. may_have_side_effects correctly returns false for the outer call...
  3. ...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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Priority

    None yet

    Effort

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions