Skip to content

[Bug]: manualPureFunctions: a pure call inside a member/new chain drops eagerly-evaluated child effects (arguments, computed keys) #10397

Description

@hyfdev

With treeshake.manualPureFunctions, a call to a listed function that sits inside a member or new chain is deleted together with subexpressions that JavaScript evaluates eagerly — call arguments, computed property keys, and the new callee chain. Their side effects are silently dropped. A bare pure call (make(effect())) is handled correctly; the bug is specific to chains.

Repro

rolldown 1.2.0 (latest release); the responsible short-circuit is unchanged on current main (4930a00).

// main.mjs
import { make } from 'ext';

function effect() {
  console.log('effect');
  return 'k';
}

make()[effect()];
make(effect()).value;
new (make()[effect()].Box)();
// rolldown.config.mjs
export default {
  input: 'main.mjs',
  external: ['ext'],
  treeshake: { manualPureFunctions: ['make'] },
  output: { file: 'out.mjs', format: 'esm' },
};

Actual output — the whole module body is gone, including the three effect() calls:

import "ext";

Expected: deleting the pure calls themselves is fine, but the eagerly-evaluated effect() subexpressions must be kept. Each isolated statement reproduces on its own.

Controls:

  • Without manualPureFunctions, all three statements are retained.
  • A bare call is already handled correctly: make(effect()); compiles to effect();.

Comparison with Rollup

Rollup 4.62.2 with the identical input and config retains all three statements. Rollup's documentation promises this in the manualPureFunctions warning:

If you pass arguments to such a pure function, those arguments are still checked for direct side effects like mutating a variable or calling a global function, in which case the call to the pure function is retained.

Cause

  • analyze_member_expr short-circuits to "no side effects" when the member chain's root resolves to a manual pure function, before analyzing anything inside the chain: stmt_eval_analyzer/mod.rs#L192-L194. The namespace branch directly below shows the intended pattern — its comment notes that a computed key is still evaluated and may have its own side effects, and it recurses into ComputedMemberExpression keys (#L195-L203).
  • The pure-function match is by chain root only: extract_first_part_of_member_expr_like walks through computed members, static members, and calls down to the leftmost identifier (#L832-L863).
  • The same root-match is exposed to Oxc via the MayHaveSideEffectsContext::manual_pure_functions impl (#L880-L882), which is how the new (make()[effect()].Box)() callee-chain effects get dropped.

Provenance

Found by the execution-order fuzzer while auditing manually-pure calls; reduced cases and current-target signatures are recorded in rolldown-order-fuzzer@77f196b. It reproduces with fully default options and is an ordinary tree-shaking bug, unrelated to strictExecutionOrder or wrapping. Extracted from the fuzzer-findings list in #10294 (comment).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions