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
Summary
Follow-up to #20334 / #20687. The
PureNotAppliedmarking 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
Current oxc behavior:
comments[0].content == CommentContent::PureNotAppliedRollup behavior (REPL):
treeshake.propertyReadSideEffects: falseand PURE present → statement is eliminatedtest()call; the outer member-access chain is handled separately bypropertyReadSideEffectsesbuild behavior (API docs, test fixture): esbuild's
bundler_dce_test.gonames patterns likedot_yes = /* @__PURE__ */ foo(sideEffect()).dot(bar)— theyessuffix is esbuild's explicit intent that the PURE is applied through the member chain.Root cause
set_pure_on_call_or_new_exprincrates/oxc_parser/src/js/expression.rsonly checks the AST top-level node:For
/* #__PURE__ */ test().a.b.c, the top-level AST node isStaticMemberExpression(the outermost.c), so the function returnsfalseand the comment is markedPureNotApplied. The same applies toChainExpressionpaths where the chain element is aMemberExpression.The existing
pure_comment_not_appliedtest doesn't covercall().chainpatterns, 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
PureNotAppliedto 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_exprthrough member-access nodes (and the matchingChainElementvariants). A symmetricpure_comment_applied_on_member_chaintest would mirror the existingpure_comment_not_applied.I can also send a PR if that's the preferred direction.
References
PureNotAppliedintroduction (also see fix(parser): set pure comment index after dedup check to handle lookahead/rewind #21570 for a later edge-case fix)