use std::mem;
use oxc_allocator::{CloneIn, TakeIn};
use oxc_ast::{NONE, ast::*};
use oxc_span::{GetSpan, SPAN, Span};
use oxc_traverse::{Ancestor, BoundIdentifier, MaybeBoundIdentifier, Traverse};
use crate::{
common::var_declarations::VarDeclarationsStore, context::TraverseCtx, state::TransformState,
utils::ast_builder::wrap_expression_in_arrow_function_iife,
};
#[derive(Debug)]
enum CallContext<'a> {
None,
This,
Binding(MaybeBoundIdentifier<'a>),
}
pub struct OptionalChaining<'a> {
is_inside_function_parameter: bool,
temp_binding: Option<BoundIdentifier<'a>>,
call_context: CallContext<'a>,
}
impl OptionalChaining<'_> {
pub fn new() -> Self {
Self {
is_inside_function_parameter: false,
temp_binding: None,
call_context: CallContext::None,
}
}
}
impl<'a> Traverse<'a, TransformState<'a>> for OptionalChaining<'a> {
#[inline]
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
match expr {
Expression::ChainExpression(_) => self.transform_chain_expression(expr, ctx),
Expression::UnaryExpression(unary_expr)
if unary_expr.operator == UnaryOperator::Delete
&& matches!(unary_expr.argument, Expression::ChainExpression(_)) =>
{
self.transform_update_expression(expr, ctx);
}
_ => {}
}
}
#[inline]
fn enter_formal_parameters(&mut self, _: &mut FormalParameters<'a>, _: &mut TraverseCtx<'a>) {
self.is_inside_function_parameter = true;
}
#[inline]
fn exit_formal_parameters(
&mut self,
_node: &mut FormalParameters<'a>,
_ctx: &mut TraverseCtx<'a>,
) {
self.is_inside_function_parameter = false;
}
}
impl<'a> OptionalChaining<'a> {
fn set_temp_binding(&mut self, binding: BoundIdentifier<'a>) {
self.temp_binding.replace(binding);
}
fn set_binding_context(&mut self, binding: MaybeBoundIdentifier<'a>) {
self.call_context = CallContext::Binding(binding);
}
fn set_this_context(&mut self) {
self.call_context = CallContext::This;
}
fn get_call_context(&self, ctx: &mut TraverseCtx<'a>) -> Argument<'a> {
debug_assert!(!matches!(self.call_context, CallContext::None));
Argument::from(if let CallContext::Binding(binding) = &self.call_context {
binding.create_read_expression(ctx)
} else {
ctx.ast.expression_this(SPAN)
})
}
fn should_specify_context(
&self,
ident: &IdentifierReference<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
match &self.call_context {
CallContext::None => false,
CallContext::This => true,
CallContext::Binding(binding) => {
binding.name != ident.name
|| binding.symbol_id.is_some_and(|symbol_id| {
ctx.scoping()
.get_reference(ident.reference_id())
.symbol_id()
.is_some_and(|id| id != symbol_id)
})
}
}
}
#[expect(clippy::unused_self)]
fn get_existing_binding_for_identifier(
&self,
ident: &IdentifierReference<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<MaybeBoundIdentifier<'a>> {
let binding = MaybeBoundIdentifier::from_identifier_reference(ident, ctx);
if ctx.state.assumptions.pure_getters
|| binding.to_bound_identifier().is_some()
|| ident.name == "eval"
{
Some(binding)
} else {
None
}
}
#[expect(clippy::unused_self)]
fn wrap_null_check(&self, left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> {
let operator = if ctx.state.assumptions.no_document_all {
BinaryOperator::Equality
} else {
BinaryOperator::StrictEquality
};
ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.expression_null_literal(SPAN))
}
fn wrap_void0_check(left: Expression<'a>, ctx: &TraverseCtx<'a>) -> Expression<'a> {
let operator = BinaryOperator::StrictEquality;
ctx.ast.expression_binary(SPAN, left, operator, ctx.ast.void_0(SPAN))
}
fn wrap_optional_check(
&self,
left1: Expression<'a>,
left2: Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let null_check = self.wrap_null_check(left1, ctx);
let void0_check = Self::wrap_void0_check(left2, ctx);
Self::create_logical_expression(null_check, void0_check, ctx)
}
fn create_logical_expression(
left: Expression<'a>,
right: Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
ctx.ast.expression_logical(SPAN, left, LogicalOperator::Or, right)
}
fn create_conditional_expression(
is_delete: bool,
test: Expression<'a>,
alternate: Expression<'a>,
span: Span,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let consequent = if is_delete {
ctx.ast.expression_boolean_literal(SPAN, true)
} else {
ctx.ast.void_0(SPAN)
};
ctx.ast.expression_conditional(span, test, consequent, alternate)
}
#[inline]
fn convert_chain_expression_to_expression(
expr: &mut Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let Expression::ChainExpression(chain_expr) = expr.take_in(ctx.ast) else { unreachable!() };
match chain_expr.unbox().expression {
element @ match_member_expression!(ChainElement) => {
Expression::from(element.into_member_expression())
}
ChainElement::CallExpression(call) => Expression::CallExpression(call),
ChainElement::TSNonNullExpression(non_null) => non_null.unbox().expression,
}
}
fn create_assignment_expression(
left: AssignmentTarget<'a>,
right: Expression<'a>,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
ctx.ast.expression_assignment(SPAN, AssignmentOperator::Assign, left, right)
}
fn transform_chain_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
*expr = if self.is_inside_function_parameter {
wrap_expression_in_arrow_function_iife(expr.take_in(ctx.ast), ctx)
} else {
self.transform_chain_expression_impl(false, expr, ctx)
}
}
fn transform_update_expression(
&mut self,
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) {
*expr = if self.is_inside_function_parameter {
wrap_expression_in_arrow_function_iife(expr.take_in(ctx.ast), ctx)
} else {
let Expression::UnaryExpression(unary_expr) = expr else { unreachable!() };
self.transform_chain_expression_impl(true, &mut unary_expr.argument, ctx)
}
}
fn transform_chain_expression_impl(
&mut self,
is_delete: bool,
chain_expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let span = chain_expr.span();
let mut chain_expr = Self::convert_chain_expression_to_expression(chain_expr, ctx);
let left =
self.transform_chain_element_recursion(&mut chain_expr, ctx).unwrap_or_else(|| {
unreachable!(
"Given chain expression certainly contains at least one optional expression,
so it must return a transformed expression"
)
});
if is_delete {
chain_expr = ctx.ast.expression_unary(SPAN, UnaryOperator::Delete, chain_expr);
}
if ctx.parent().is_parenthesized_expression()
&& matches!(ctx.ancestor(1), Ancestor::CallExpressionCallee(_))
{
chain_expr = self.transform_expression_to_bind_context(chain_expr, ctx);
}
self.temp_binding = None;
self.call_context = CallContext::None;
Self::create_conditional_expression(is_delete, left, chain_expr, span, ctx)
}
fn transform_expression_to_bind_context(
&self,
mut expr: Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let context = if let Some(member) = expr.as_member_expression_mut() {
let object = member.object_mut().get_inner_expression_mut();
let context = if ctx.state.assumptions.pure_getters {
object.clone_in(ctx.ast.allocator)
} else if let Expression::Identifier(ident) = object {
MaybeBoundIdentifier::from_identifier_reference(ident, ctx)
.create_read_expression(ctx)
} else {
let binding = VarDeclarationsStore::create_uid_var_based_on_node(object, ctx);
*object = Self::create_assignment_expression(
binding.create_write_target(ctx),
object.take_in(ctx.ast),
ctx,
);
binding.create_read_expression(ctx)
};
Argument::from(context)
} else {
self.get_call_context(ctx)
};
let arguments = ctx.ast.vec1(context);
let property = ctx.ast.identifier_name(SPAN, "bind");
let callee = ctx.ast.member_expression_static(SPAN, expr, property, false);
let callee = Expression::from(callee);
ctx.ast.expression_call(SPAN, callee, NONE, arguments, false)
}
fn transform_chain_element_recursion(
&mut self,
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let expr = expr.get_inner_expression_mut();
match expr {
Expression::ChainExpression(_) => {
*expr = Self::convert_chain_expression_to_expression(expr, ctx);
self.transform_chain_element_recursion(expr, ctx)
}
Expression::StaticMemberExpression(member) => {
let left = self.transform_chain_element_recursion(&mut member.object, ctx);
if member.optional {
member.optional = false;
Some(self.transform_optional_expression(false, left, &mut member.object, ctx))
} else {
left
}
}
Expression::ComputedMemberExpression(member) => {
let left = self.transform_chain_element_recursion(&mut member.object, ctx);
if member.optional {
member.optional = false;
Some(self.transform_optional_expression(false, left, &mut member.object, ctx))
} else {
left
}
}
Expression::PrivateFieldExpression(member) => {
let left = self.transform_chain_element_recursion(&mut member.object, ctx);
if member.optional {
member.optional = false;
Some(self.transform_optional_expression(false, left, &mut member.object, ctx))
} else {
left
}
}
Expression::CallExpression(call) => {
let left = self.transform_chain_element_recursion(&mut call.callee, ctx);
if call.optional {
call.optional = false;
let callee = call.callee.get_inner_expression_mut();
let left = Some(self.transform_optional_expression(true, left, callee, ctx));
if !ctx.state.assumptions.pure_getters {
if let Expression::Identifier(ident) = callee
&& self.should_specify_context(ident, ctx)
{
let callee = callee.take_in(ctx.ast);
let property = ctx.ast.identifier_name(SPAN, "call");
let member =
ctx.ast.member_expression_static(SPAN, callee, property, false);
call.callee = Expression::from(member);
call.arguments.insert(0, self.get_call_context(ctx));
}
}
left
} else {
left
}
}
_ => None,
}
}
fn transform_optional_expression(
&mut self,
is_call: bool,
left: Option<Expression<'a>>,
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if let Some(left) = left {
return self.transform_and_join_expression(is_call, left, expr, ctx);
}
let expr = expr.get_inner_expression_mut();
if let Expression::Identifier(ident) = expr
&& let Some(binding) = self.get_existing_binding_for_identifier(ident, ctx)
{
if ident.name == "eval" {
let zero = ctx.ast.number_0();
let original_callee = expr.take_in(ctx.ast);
let expressions = ctx.ast.vec_from_array([zero, original_callee]);
*expr = ctx.ast.expression_sequence(SPAN, expressions);
}
let left1 = binding.create_read_expression(ctx);
let replacement = if ctx.state.assumptions.no_document_all {
self.wrap_null_check(left1, ctx)
} else {
let left2 = binding.create_read_expression(ctx);
self.wrap_optional_check(left1, left2, ctx)
};
self.set_binding_context(binding);
return replacement;
}
let temp_binding = VarDeclarationsStore::create_uid_var_based_on_node(expr, ctx);
if is_call && !ctx.state.assumptions.pure_getters {
self.set_chain_call_context(expr, ctx);
}
let expr = mem::replace(expr, temp_binding.create_read_expression(ctx));
let assignment_expression =
Self::create_assignment_expression(temp_binding.create_write_target(ctx), expr, ctx);
let expr = if ctx.state.assumptions.no_document_all {
self.wrap_null_check(assignment_expression, ctx)
} else {
self.wrap_optional_check(
assignment_expression,
temp_binding.create_read_expression(ctx),
ctx,
)
};
self.set_temp_binding(temp_binding);
expr
}
fn transform_and_join_expression(
&mut self,
is_call: bool,
left: Expression<'a>,
expr: &mut Expression<'a>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
if is_call {
if let Some(temp_binding) = self.temp_binding.take() {
self.set_binding_context(temp_binding.to_maybe_bound_identifier());
}
self.set_chain_call_context(expr, ctx);
}
let temp_binding = {
if self.temp_binding.is_none() {
let binding = VarDeclarationsStore::create_uid_var_based_on_node(expr, ctx);
self.set_temp_binding(binding);
}
self.temp_binding.as_ref().unwrap()
};
let expr = mem::replace(expr, temp_binding.create_read_expression(ctx));
let assignment_expression =
Self::create_assignment_expression(temp_binding.create_write_target(ctx), expr, ctx);
let left = Self::create_logical_expression(
left,
self.wrap_null_check(assignment_expression, ctx),
ctx,
);
if ctx.state.assumptions.no_document_all {
left
} else {
let reference = temp_binding.create_read_expression(ctx);
Self::create_logical_expression(left, Self::wrap_void0_check(reference, ctx), ctx)
}
}
fn set_chain_call_context(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if let Some(member) = expr.as_member_expression_mut() {
let object = member.object_mut();
if matches!(object, Expression::Super(_) | Expression::ThisExpression(_)) {
self.set_this_context();
} else {
let binding = object
.get_identifier_reference()
.and_then(|ident| self.get_existing_binding_for_identifier(ident, ctx))
.unwrap_or_else(|| {
let binding =
VarDeclarationsStore::create_uid_var_based_on_node(object, ctx);
*object = Self::create_assignment_expression(
binding.create_write_target(ctx),
object.take_in(ctx.ast),
ctx,
);
binding.to_maybe_bound_identifier()
});
self.set_binding_context(binding);
}
}
}
}