use oxc_allocator::{Box as ArenaBox, TakeIn};
use oxc_ast::{NONE, ast::*};
use oxc_semantic::{ScopeFlags, SymbolFlags};
use oxc_span::SPAN;
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};
use oxc_traverse::{Ancestor, BoundIdentifier, Traverse};
use crate::{context::TraverseCtx, state::TransformState};
pub struct NullishCoalescingOperator;
impl NullishCoalescingOperator {
pub fn new() -> Self {
Self
}
}
impl<'a> Traverse<'a, TransformState<'a>> for NullishCoalescingOperator {
fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
if !matches!(expr, Expression::LogicalExpression(logical_expr) if logical_expr.operator == LogicalOperator::Coalesce)
{
return;
}
let Expression::LogicalExpression(logical_expr) = expr.take_in(ctx.ast) else {
unreachable!()
};
*expr = self.transform_logical_expression(logical_expr, ctx);
}
}
impl<'a> NullishCoalescingOperator {
#[expect(clippy::unused_self)]
fn transform_logical_expression(
&self,
logical_expr: ArenaBox<'a, LogicalExpression<'a>>,
ctx: &mut TraverseCtx<'a>,
) -> Expression<'a> {
let logical_expr = logical_expr.unbox();
match &logical_expr.left {
Expression::ThisExpression(this) => {
let this_span = this.span;
return Self::create_conditional_expression(
logical_expr.left,
ctx.ast.expression_this(this_span),
ctx.ast.expression_this(this_span),
logical_expr.right,
logical_expr.span,
ctx,
);
}
Expression::Identifier(ident) => {
let symbol_id = ctx.scoping().get_reference(ident.reference_id()).symbol_id();
if let Some(symbol_id) = symbol_id {
if ctx.scoping().get_resolved_references(symbol_id).all(|r| !r.is_write()) {
let binding = BoundIdentifier::new(ident.name, symbol_id);
let ident_span = ident.span;
return Self::create_conditional_expression(
logical_expr.left,
binding.create_spanned_read_expression(ident_span, ctx),
binding.create_spanned_read_expression(ident_span, ctx),
logical_expr.right,
logical_expr.span,
ctx,
);
}
}
}
_ => {}
}
let is_parent_formal_parameter =
matches!(ctx.ancestor(0), Ancestor::FormalParameterInitializer(_));
let current_scope_id = if is_parent_formal_parameter {
ctx.create_child_scope_of_current(ScopeFlags::Arrow | ScopeFlags::Function)
} else {
ctx.current_hoist_scope_id()
};
let binding = ctx.generate_uid_based_on_node(
&logical_expr.left,
current_scope_id,
SymbolFlags::FunctionScopedVariable,
);
let assignment = ctx.ast.expression_assignment(
SPAN,
AssignmentOperator::Assign,
binding.create_write_target(ctx),
logical_expr.left,
);
let mut new_expr = Self::create_conditional_expression(
assignment,
binding.create_read_expression(ctx),
binding.create_read_expression(ctx),
logical_expr.right,
logical_expr.span,
ctx,
);
if is_parent_formal_parameter {
let id = binding.create_binding_pattern(ctx);
let param = ctx.ast.formal_parameter(
SPAN,
ctx.ast.vec(),
id,
NONE,
NONE,
false,
None,
false,
false,
);
let params = ctx.ast.formal_parameters(
SPAN,
FormalParameterKind::ArrowFormalParameters,
ctx.ast.vec1(param),
NONE,
);
let body = ctx.ast.function_body(
SPAN,
ctx.ast.vec(),
ctx.ast.vec1(ctx.ast.statement_expression(SPAN, new_expr)),
);
let arrow_function = ctx.ast.expression_arrow_function_with_scope_id_and_pure_and_pife(
SPAN,
true,
false,
NONE,
params,
NONE,
body,
current_scope_id,
false,
false,
);
new_expr = ctx.ast.expression_call(SPAN, arrow_function, NONE, ctx.ast.vec(), false);
} else {
ctx.state.var_declarations.insert_var(&binding, ctx.ast);
}
new_expr
}
fn create_conditional_expression(
assignment: Expression<'a>,
reference1: Expression<'a>,
reference2: Expression<'a>,
default: Expression<'a>,
span: Span,
ctx: &TraverseCtx<'a>,
) -> Expression<'a> {
let op = BinaryOperator::StrictInequality;
let null = ctx.ast.expression_null_literal(SPAN);
let left = ctx.ast.expression_binary(SPAN, assignment, op, null);
let right = ctx.ast.expression_binary(SPAN, reference1, op, ctx.ast.void_0(SPAN));
let test = ctx.ast.expression_logical(SPAN, left, LogicalOperator::And, right);
ctx.ast.expression_conditional(span, test, reference2, default)
}
}