Skip to content

fix(minifier): remove pure-annotated initializers of unused declarations#22251

Closed
mo-n wants to merge 1 commit into
oxc-project:mainfrom
mo-n:main
Closed

fix(minifier): remove pure-annotated initializers of unused declarations#22251
mo-n wants to merge 1 commit into
oxc-project:mainfrom
mo-n:main

Conversation

@mo-n

@mo-n mo-n commented May 8, 2026

Copy link
Copy Markdown

fix(minifier): respect @__PURE__ annotations when removing unused variable declarations

Summary

Fixes #22255

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.

The Bug

Given this input:

// With CompressOptions::smallest() (unused: Remove)
var x = /* @__PURE__ */ (() => {
  return oprt(1);
})();

Before this fix (incorrect output):

oprt(1);

After this fix (correct output):

// (empty — fully removed)

Root Cause

In handle_variable_declaration (minimize_statements.rs), when an unused declarator is removed, its initializer is checked via init.may_have_side_effects(ctx) to decide whether to keep it as an expression statement:

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)  // ← the issue
    {
        result.push(ctx.ast.statement_expression(init.span(), init));
    }
}

While CallExpression::may_have_side_effects does respect @__PURE__ annotations for direct calls with no arguments, this approach misses the deeper expression-level optimizations that remove_unused_expression provides — such as handling pure IIFEs, stripping pure calls while preserving their side-effectful arguments, and simplifying nested expressions.

The real 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

By using remove_unused_expression instead, we leverage the full expression simplification pipeline that properly handles these cases.

Fix

Replace init.may_have_side_effects(ctx) with !Self::remove_unused_expression(&mut init, ctx) in handle_variable_declaration. This delegates to the same expression removal logic used everywhere else in the minifier, which 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)

Test Cases Added

// All fully removed (unused + pure):
test_options("var x = /* @__PURE__ */ foo()", "", &options);
test_options("var x = /* @__PURE__ */ new Foo()", "", &options);
test_options("var x = /* @__PURE__ */ (() => { return foo() })()", "", &options);
test_options("var x = /* @__PURE__ */ (() => { return oprt(1) })()", "", &options);
test_options("var x = /* @__PURE__ */ (function() { return foo() })()", "", &options);

// Side-effectful arguments preserved:
test_options("var x = /* @__PURE__ */ foo(a)", "a", &options);
test_options("var x = /* @__PURE__ */ foo(bar())", "bar()", &options);

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 a side-effectful statement, causing the entire module and its dependencies to be included in the final bundle even when unused.

AI Disclosure

AI tools were used to assist with code review and identifying the missing guards in is_in_unused_variable_declarator. All changes have been reviewed, tested, and verified by the contributor.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5752d710a2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs
@mo-n mo-n force-pushed the main branch 2 times, most recently from add04d1 to f47c21e Compare May 8, 2026 08:14
Previously, unused variable declarations with `@__PURE__` initializers
(e.g. pure IIFEs) would have their bodies incorrectly preserved as
side-effectful expression statements. This replaces `may_have_side_effects`
with `remove_unused_expression` and adds an `is_in_unused_variable_declarator`
guard that reuses `can_remove_unused_declarators` to respect script-mode and
eval constraints.
@camc314 camc314 added the A-minifier Area - Minifier label May 8, 2026
@Dunqing

Dunqing commented May 8, 2026

Copy link
Copy Markdown
Member

Can you create an issue first?

@codspeed-hq

codspeed-hq Bot commented May 11, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 44 untouched benchmarks
⏩ 7 skipped benchmarks1


Comparing mo-n:main (d38c213) with main (7e93bf2)2

Open in CodSpeed

Footnotes

  1. 7 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

  2. No successful run was found on main (22db602) during the generation of this report, so 7e93bf2 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-minifier Area - Minifier

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

3 participants