Skip to content

parser: PureNotApplied marks chained-call patterns that Rollup/esbuild consider valid #22394

@Kyujenius

Description

@Kyujenius

Summary

Follow-up to #20334 / #20687. The PureNotApplied marking introduced in #20687 covers three contexts; for the expression-level context, it currently flags chained-call patterns (e.g. /* #__PURE__ */ test().a.b.c) as not-applied, but Rollup and esbuild consider these valid PURE annotations.

Reproduction

/*#__PURE__*/ test().a.b.c;

Current oxc behavior: comments[0].content == CommentContent::PureNotApplied

Rollup behavior (REPL):

  • With treeshake.propertyReadSideEffects: false and PURE present → statement is eliminated
  • Same options, PURE removed → statement is preserved
  • → Rollup applies PURE to the innermost test() call; the outer member-access chain is handled separately by propertyReadSideEffects

esbuild behavior (API docs, test fixture): esbuild's bundler_dce_test.go names patterns like dot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar) — the yes suffix is esbuild's explicit intent that the PURE is applied through the member chain.

Root cause

set_pure_on_call_or_new_expr in crates/oxc_parser/src/js/expression.rs only checks the AST top-level node:

fn set_pure_on_call_or_new_expr(expr: &mut Expression<'a>) -> bool {
    match &mut expr.get_inner_expression_mut() {
        Expression::CallExpression(call_expr) => { call_expr.pure = true; true }
        Expression::NewExpression(new_expr)   => { new_expr.pure = true; true }
        Expression::BinaryExpression(b)       => Self::set_pure_on_call_or_new_expr(&mut b.left),
        Expression::LogicalExpression(l)      => Self::set_pure_on_call_or_new_expr(&mut l.left),
        Expression::ConditionalExpression(c)  => Self::set_pure_on_call_or_new_expr(&mut c.test),
        Expression::ChainExpression(chain)    => {
            if let ChainElement::CallExpression(c) = &mut chain.expression { c.pure = true; true } else { false }
        }
        _ => false,  // ← StaticMember/Computed/PrivateField land here
    }
}

For /* #__PURE__ */ test().a.b.c, the top-level AST node is StaticMemberExpression (the outermost .c), so the function returns false and the comment is marked PureNotApplied. The same applies to ChainExpression paths where the chain element is a MemberExpression.

The existing pure_comment_not_applied test doesn't cover call().chain patterns, suggesting this may be incidental rather than intentional.

Question for the maintainers

Is the current strict matching intentional (the spec-like definition: "PURE is valid only when the top-level expression is itself a CallExpression/NewExpression"), or is alignment with Rollup/esbuild semantics preferred?

The downstream impact is that consumers forwarding PureNotApplied to user-facing warnings (Rolldown — see rolldown/rolldown#9381) emit false-positive warnings on patterns Rollup/esbuild accept.

Suggested fix (if alignment is preferred)

The natural fix would be to recurse set_pure_on_call_or_new_expr through member-access nodes (and the matching ChainElement variants). A symmetric pure_comment_applied_on_member_chain test would mirror the existing pure_comment_not_applied.

I can also send a PR if that's the preferred direction.

References

Metadata

Metadata

Assignees

Labels

Type

No type

Priority

None yet

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions