use oxc_allocator::{ArenaVec, TakeIn};
use oxc_ast::ast::*;
use oxc_ecmascript::{
GlobalContext, ToJsString,
constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType, ValueType},
side_effects::MayHaveSideEffects,
};
use oxc_span::{GetSpan, SPAN};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};
use crate::TraverseCtx;
use super::PeepholeOptimizations;
impl<'a> PeepholeOptimizations {
#[expect(clippy::float_cmp)]
pub fn fold_unary_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::UnaryExpression(e) = expr else { return };
match e.operator {
UnaryOperator::Void if e.argument.is_number_0() => {}
UnaryOperator::LogicalNot if matches!(&e.argument, Expression::NumericLiteral(lit) if lit.value == 0.0 || lit.value == 1.0) =>
{}
UnaryOperator::UnaryNegation if e.argument.is_big_int_literal() => {}
_ if e.may_have_side_effects(ctx) => {}
_ => {
if let Some(changed) = e.evaluate_value(ctx).map(|v| ctx.value_to_expr(e.span, v)) {
ctx.replace_expression(expr, changed);
}
}
}
}
pub fn fold_static_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::StaticMemberExpression(e) = expr else { return };
let Some(value) = e.evaluate_value(ctx) else { return };
if e.object.may_have_side_effects(ctx) {
return;
}
let changed = ctx.value_to_expr(e.span, value);
ctx.replace_expression(expr, changed);
}
pub fn fold_computed_member_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ComputedMemberExpression(e) = expr else { return };
let Some(value) = e.evaluate_value(ctx) else { return };
if e.object.may_have_side_effects(ctx) || e.expression.may_have_side_effects(ctx) {
return;
}
let changed = ctx.value_to_expr(e.span, value);
ctx.replace_expression(expr, changed);
}
pub fn fold_logical_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::LogicalExpression(e) = expr else { return };
if let Some(changed) = match e.operator {
LogicalOperator::And | LogicalOperator::Or => Self::try_fold_and_or(e, ctx),
LogicalOperator::Coalesce => Self::try_fold_coalesce(e, ctx),
} {
ctx.replace_expression(expr, changed);
}
}
pub fn fold_chain_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::ChainExpression(e) = expr else { return };
let span = e.span;
match try_fold_chain_at_element(&mut e.expression, ctx) {
ChainFold::Unfolded { .. } => {}
ChainFold::Flipped { has_optional } => {
if has_optional {
ctx.notice_change();
} else {
let new_expr = Expression::from(e.expression.take_in(ctx));
ctx.replace_expression(expr, new_expr);
}
}
ChainFold::Collapse { base, base_has_side_effects } => {
let new_expr = if base_has_side_effects {
Expression::new_sequence_expression(
span,
ArenaVec::from_array_in([base, Expression::new_void_0(span, ctx)], ctx),
ctx,
)
} else {
ctx.drop_expression(&base);
ctx.value_to_expr(span, ConstantValue::Undefined)
};
ctx.replace_expression(expr, new_expr);
}
}
}
pub fn try_fold_and_or(
logical_expr: &mut LogicalExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let op = logical_expr.operator;
debug_assert!(matches!(op, LogicalOperator::And | LogicalOperator::Or));
let left = &logical_expr.left;
let left_val = left.evaluate_value_to_boolean(ctx);
if let Some(lval) = left_val {
if if lval { op.is_or() } else { op.is_and() } {
if !lval && op.is_and() && is_cjs_module_exports_hint(&logical_expr.right) {
return None;
}
return Some(logical_expr.left.take_in(ctx));
} else if !left.may_have_side_effects(ctx) {
let should_keep_indirect_access =
Self::should_keep_indirect_access(&logical_expr.right, ctx);
if should_keep_indirect_access {
return Some(Expression::new_sequence_expression(
logical_expr.span,
ArenaVec::from_array_in(
[
Expression::new_numeric_literal(
logical_expr.left.span(),
0.0,
None,
NumberBase::Decimal,
ctx,
),
logical_expr.right.take_in(ctx),
],
ctx,
),
ctx,
));
}
return Some(logical_expr.right.take_in(ctx));
}
let left = logical_expr.left.take_in(ctx);
let right = logical_expr.right.take_in(ctx);
let vec = ArenaVec::from_array_in([left, right], ctx);
let sequence_expr = Expression::new_sequence_expression(logical_expr.span, vec, ctx);
return Some(sequence_expr);
} else if let Expression::LogicalExpression(left_child) = &mut logical_expr.left
&& left_child.operator == logical_expr.operator
{
let left_child_right_boolean = left_child.right.evaluate_value_to_boolean(ctx);
let left_child_op = left_child.operator;
if let Some(right_boolean) = left_child_right_boolean
&& !left_child.right.may_have_side_effects(ctx)
{
if !right_boolean && left_child_op.is_or()
|| right_boolean && left_child_op.is_and()
{
let left = left_child.left.take_in(ctx);
let right = logical_expr.right.take_in(ctx);
let logic_expr = Expression::new_logical_expression(
logical_expr.span,
left,
left_child_op,
right,
ctx,
);
return Some(logic_expr);
}
}
}
None
}
pub fn try_fold_coalesce(
logical_expr: &mut LogicalExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce);
let left = &logical_expr.left;
let left_val = left.value_type(ctx);
match left_val {
ValueType::Null | ValueType::Undefined => {
Some(if left.may_have_side_effects(ctx) {
let expressions = ArenaVec::from_array_in(
[logical_expr.left.take_in(ctx), logical_expr.right.take_in(ctx)],
ctx,
);
Expression::new_sequence_expression(logical_expr.span, expressions, ctx)
} else {
let should_keep_indirect_access =
Self::should_keep_indirect_access(&logical_expr.right, ctx);
if should_keep_indirect_access {
return Some(Expression::new_sequence_expression(
logical_expr.span,
ArenaVec::from_array_in(
[
Expression::new_numeric_literal(
logical_expr.left.span(),
0.0,
None,
NumberBase::Decimal,
ctx,
),
logical_expr.right.take_in(ctx),
],
ctx,
),
ctx,
));
}
logical_expr.right.take_in(ctx)
})
}
ValueType::Number
| ValueType::BigInt
| ValueType::String
| ValueType::Boolean
| ValueType::Object => {
let should_keep_indirect_access =
Self::should_keep_indirect_access(&logical_expr.left, ctx);
if should_keep_indirect_access {
return Some(Expression::new_sequence_expression(
logical_expr.span,
ArenaVec::from_array_in(
[
Expression::new_numeric_literal(
logical_expr.right.span(),
0.0,
None,
NumberBase::Decimal,
ctx,
),
logical_expr.left.take_in(ctx),
],
ctx,
),
ctx,
));
}
Some(logical_expr.left.take_in(ctx))
}
ValueType::Undetermined => None,
}
}
fn extract_numeric_values(e: &BinaryExpression<'a>) -> Option<(f64, f64)> {
if let (Expression::NumericLiteral(left), Expression::NumericLiteral(right)) =
(&e.left, &e.right)
{
return Some((left.value, right.value));
}
None
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
pub fn fold_binary_expr(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
let span = e.span;
let changed = match e.operator {
BinaryOperator::Equality
| BinaryOperator::Inequality
| BinaryOperator::StrictEquality
| BinaryOperator::StrictInequality
| BinaryOperator::LessThan
| BinaryOperator::GreaterThan
| BinaryOperator::LessEqualThan
| BinaryOperator::GreaterEqualThan
| BinaryOperator::ShiftRight
| BinaryOperator::Instanceof => ctx.eval_binary(e),
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR => {
ctx.eval_binary(e).or_else(|| Self::try_fold_left_child_op(e, ctx))
}
BinaryOperator::Addition => Self::try_fold_add(e, ctx),
BinaryOperator::Subtraction => {
Self::extract_numeric_values(e)
.filter(|(left, right)| {
left.is_nan()
|| left.is_finite()
|| right.is_nan()
|| right.is_finite()
|| (left.fract() == 0.0
&& right.fract() == 0.0
&& (left.abs() as usize) <= 0xFFFF_FFFF
&& (right.abs() as usize) <= 0xFFFF_FFFF)
})
.and_then(|_| ctx.eval_binary(e))
}
BinaryOperator::Multiplication
| BinaryOperator::Exponential
| BinaryOperator::Remainder => Self::extract_numeric_values(e)
.filter(|(left, right)| {
*left == 0.0
|| left.is_nan()
|| left.is_infinite()
|| *right == 0.0
|| right.is_nan()
|| right.is_infinite()
|| (e.operator == BinaryOperator::Multiplication
&& left.abs() <= 255.0
&& left.fract() == 0.0
&& right.abs() <= 255.0
&& right.fract() == 0.0)
})
.and_then(|_| ctx.eval_binary(e)),
BinaryOperator::Division => Self::extract_numeric_values(e)
.filter(|(_, right)| *right == 0.0 || right.is_nan() || right.is_infinite())
.and_then(|_| ctx.eval_binary(e)),
BinaryOperator::ShiftLeft => {
Self::extract_numeric_values(e).and_then(|(left, right)| {
let result = e.evaluate_value(ctx)?.into_number()?;
let left_len = Self::approximate_printed_int_char_count(left);
let right_len = Self::approximate_printed_int_char_count(right);
let result_len = Self::approximate_printed_int_char_count(result);
(result_len <= left_len + 2 + right_len)
.then(|| ctx.value_to_expr(span, ConstantValue::Number(result)))
})
}
BinaryOperator::ShiftRightZeroFill => {
Self::extract_numeric_values(e).and_then(|(left, right)| {
let result = e.evaluate_value(ctx)?.into_number()?;
let left_len = Self::approximate_printed_int_char_count(left);
let right_len = Self::approximate_printed_int_char_count(right);
let result_len = Self::approximate_printed_int_char_count(result);
(result_len <= left_len + 3 + right_len)
.then(|| ctx.value_to_expr(span, ConstantValue::Number(result)))
})
}
BinaryOperator::In => None,
};
if let Some(changed) = changed {
ctx.replace_expression(expr, changed);
}
}
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
#[must_use]
fn approximate_printed_int_char_count(value: f64) -> usize {
let mut count = if value.is_infinite() {
"Infinity".len()
} else if value.is_nan() {
"NaN".len()
} else {
1 + value.abs().log10().floor() as usize
};
if value.is_sign_negative() {
count += 1;
}
count
}
fn try_fold_add(e: &mut BinaryExpression<'a>, ctx: &TraverseCtx<'a>) -> Option<Expression<'a>> {
if !e.may_have_side_effects(ctx)
&& let Some(v) = e.evaluate_value(ctx)
{
return Some(ctx.value_to_expr(e.span, v));
}
debug_assert_eq!(e.operator, BinaryOperator::Addition);
if let Some(expr) = Self::try_fold_add_op(&mut e.left, &mut e.right, e.span, ctx) {
return Some(expr);
}
if let Expression::BinaryExpression(left_binary_expr) = &mut e.left
&& left_binary_expr.operator == BinaryOperator::Addition
&& left_binary_expr.right.value_type(ctx).is_string()
{
if let (Some(left_str), Some(right_str)) = (
left_binary_expr.right.get_side_free_string_value(ctx),
e.right.get_side_free_string_value(ctx),
) {
let span = left_binary_expr
.right
.span()
.merge_within(e.right.span(), e.span)
.unwrap_or(SPAN);
let value = Str::from_strs_array_in([&left_str, &right_str], ctx);
let right = Expression::new_string_literal(span, value, None, ctx);
let left = left_binary_expr.left.take_in(ctx);
return Some(Expression::new_binary_expression(
e.span, left, e.operator, right, ctx,
));
}
if let Some(new_right) =
Self::try_fold_add_op(&mut left_binary_expr.right, &mut e.right, e.span, ctx)
{
let left = left_binary_expr.left.take_in(ctx);
return Some(Expression::new_binary_expression(
e.span, left, e.operator, new_right, ctx,
));
}
}
None
}
fn try_fold_add_op(
left_expr: &mut Expression<'a>,
right_expr: &mut Expression<'a>,
parent_span: Span,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
if let Expression::TemplateLiteral(left) = left_expr {
if let Expression::TemplateLiteral(right) = right_expr {
left.span = left.span.merge_within(right.span, parent_span).unwrap_or(SPAN);
let left_last_quasi =
left.quasis.last_mut().expect("template literal must have at least one quasi");
let right_first_quasi = right
.quasis
.first_mut()
.expect("template literal must have at least one quasi");
left_last_quasi.value.raw = Str::from_strs_array_in(
[left_last_quasi.value.raw.as_str(), right_first_quasi.value.raw.as_str()],
ctx,
);
let new_cooked = if let (Some(cooked1), Some(cooked2)) =
(left_last_quasi.value.cooked, right_first_quasi.value.cooked)
{
Some(Str::from_strs_array_in([cooked1.as_str(), cooked2.as_str()], ctx))
} else {
None
};
left_last_quasi.value.cooked = new_cooked;
if !right.quasis.is_empty() {
left_last_quasi.tail = false;
}
left.quasis.extend(right.quasis.drain(1..)); left.expressions.extend(right.expressions.drain(..));
return Some(left_expr.take_in(ctx));
}
if let Some(right_str) = right_expr.get_side_free_string_value(ctx) {
left.span = left.span.merge_within(right_expr.span(), parent_span).unwrap_or(SPAN);
let last_quasi =
left.quasis.last_mut().expect("template literal must have at least one quasi");
last_quasi.value.raw = Str::from_strs_array_in(
[
last_quasi.value.raw.as_str(),
Self::escape_string_for_template_literal(&right_str).as_ref(),
],
ctx,
);
let new_cooked = last_quasi.value.cooked.map(|cooked| {
Str::from_strs_array_in([cooked.as_str(), right_str.as_ref()], ctx)
});
last_quasi.value.cooked = new_cooked;
return Some(left_expr.take_in(ctx));
}
} else if let Expression::TemplateLiteral(right) = right_expr {
if let Some(left_str) = left_expr.get_side_free_string_value(ctx) {
right.span = right.span.merge_within(left_expr.span(), parent_span).unwrap_or(SPAN);
let first_quasi = right
.quasis
.first_mut()
.expect("template literal must have at least one quasi");
first_quasi.value.raw = Str::from_strs_array_in(
[
Self::escape_string_for_template_literal(&left_str).as_ref(),
first_quasi.value.raw.as_str(),
],
ctx,
);
let new_cooked = first_quasi.value.cooked.map(|cooked| {
Str::from_strs_array_in([left_str.as_ref(), cooked.as_str()], ctx)
});
first_quasi.value.cooked = new_cooked;
return Some(right_expr.take_in(ctx));
}
}
if Self::evaluates_to_empty_string(left_expr) && right_expr.value_type(ctx).is_string() {
return Some(right_expr.take_in(ctx));
} else if Self::evaluates_to_empty_string(right_expr)
&& left_expr.value_type(ctx).is_string()
{
return Some(left_expr.take_in(ctx));
}
None
}
fn evaluates_to_empty_string(e: &Expression<'a>) -> bool {
match e {
Expression::StringLiteral(s) => s.value.is_empty(),
Expression::ArrayExpression(a) => a.elements.is_empty(),
_ => false,
}
}
fn try_fold_left_child_op(
e: &mut BinaryExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> Option<Expression<'a>> {
let op = e.operator;
debug_assert!(matches!(
op,
BinaryOperator::BitwiseAnd | BinaryOperator::BitwiseOR | BinaryOperator::BitwiseXOR
));
let Expression::BinaryExpression(left) = &mut e.left else {
return None;
};
if left.operator != op {
return None;
}
let (v, expr_to_move);
if let Some(result) = ctx.eval_binary_operation(op, &left.left, &e.right) {
(v, expr_to_move) = (result, &mut left.right);
} else if let Some(result) = ctx.eval_binary_operation(op, &left.right, &e.right) {
(v, expr_to_move) = (result, &mut left.left);
} else {
return None;
}
Some(Expression::new_binary_expression(
e.span,
expr_to_move.take_in(ctx),
op,
ctx.value_to_expr(
left.right.span().merge_within(e.right.span(), e.span).unwrap_or(SPAN),
v,
),
ctx,
))
}
pub fn fold_call_expression(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::CallExpression(e) = expr else { return };
if !ctx.is_global_expr("Number", &e.callee) {
return;
}
if e.arguments.len() != 1 {
return;
}
let Some(arg) = e.arguments[0].as_expression() else { return };
let value = ConstantValue::Number(match arg {
Expression::Identifier(ident) if ctx.is_identifier_undefined(ident) => f64::NAN,
Expression::NullLiteral(_) => 0.0,
Expression::BooleanLiteral(b) => f64::from(b.value),
Expression::NumericLiteral(n) => n.value,
Expression::StringLiteral(n) => {
if let Some(n) = arg.evaluate_value_to_number(ctx) {
n
} else {
let new_expr = Expression::new_unary_expression(
e.span,
UnaryOperator::UnaryPlus,
Expression::new_string_literal(n.span, n.value, n.raw, ctx),
ctx,
);
ctx.replace_expression(expr, new_expr);
return;
}
}
e if e.is_void_0() => f64::NAN,
_ => return,
});
let new_expr = ctx.value_to_expr(e.span, value);
ctx.replace_expression(expr, new_expr);
}
pub fn fold_binary_typeof_comparison(expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
let Expression::BinaryExpression(e) = expr else { return };
if e.operator.is_equality()
&& let (Expression::UnaryExpression(left), Expression::UnaryExpression(right)) =
(&e.left, &e.right)
&& left.operator.is_typeof()
&& right.operator.is_typeof()
&& let (Expression::Identifier(left_ident), Expression::Identifier(right_ident)) =
(&left.argument, &right.argument)
&& left_ident.name == right_ident.name
{
let b = matches!(e.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality);
let new_expr = Expression::new_boolean_literal(e.span, b, ctx);
ctx.replace_expression(expr, new_expr);
return;
}
if let Expression::UnaryExpression(left) = &e.left
&& left.operator.is_typeof()
&& e.operator.is_equality()
{
let right_ty = e.right.value_type(ctx);
if !right_ty.is_undetermined() && right_ty != ValueType::String {
let new_expr = Expression::new_boolean_literal(
e.span,
e.operator == BinaryOperator::Inequality
|| e.operator == BinaryOperator::StrictInequality,
ctx,
);
ctx.replace_expression(expr, new_expr);
return;
}
if let Expression::StringLiteral(string_lit) = &e.right
&& !matches!(
string_lit.value.as_str(),
"string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function"
| "unknown" )
{
let new_expr = Expression::new_boolean_literal(
e.span,
e.operator == BinaryOperator::Inequality
|| e.operator == BinaryOperator::StrictInequality,
ctx,
);
ctx.replace_expression(expr, new_expr);
}
}
}
pub fn fold_object_exp(e: &mut ObjectExpression<'a>, ctx: &mut TraverseCtx<'a>) {
fn should_fold_spread_element<'a>(e: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
match e {
Expression::ArrayExpression(o) if o.elements.is_empty() => true,
Expression::ArrowFunctionExpression(_) | Expression::FunctionExpression(_) => true,
e if e.is_literal() && !e.is_string_literal() => true,
e if e.evaluate_value(ctx).is_some_and(|v| !v.is_string())
&& !e.may_have_side_effects(ctx) =>
{
true
}
_ => false,
}
}
let (new_size, should_fold) =
e.properties.iter().fold((0, false), |(new_size, should_fold), p| {
let ObjectPropertyKind::SpreadProperty(spread_element) = p else {
return (new_size + 1, should_fold);
};
match &spread_element.argument {
Expression::ObjectExpression(o)
if Self::is_spread_inlineable_object_literal(o, ctx) =>
{
(new_size + o.properties.len(), true)
}
e if should_fold_spread_element(e, ctx) => (new_size, true),
_ => (new_size + 1, should_fold),
}
});
if !should_fold {
return;
}
let mut new_properties = ArenaVec::<ObjectPropertyKind>::with_capacity_in(new_size, ctx);
for p in e.properties.drain(..) {
if let ObjectPropertyKind::SpreadProperty(mut spread_element) = p {
if ctx.is_expression_undefined(&spread_element.argument) {
ctx.drop_expression(&spread_element.argument);
continue;
}
match &mut spread_element.argument {
Expression::ObjectExpression(o)
if Self::is_spread_inlineable_object_literal(o, ctx) =>
{
for prop in o.properties.drain(..) {
match &prop {
ObjectPropertyKind::SpreadProperty(_) => {
new_properties.push(prop);
}
ObjectPropertyKind::ObjectProperty(p)
if p.computed
|| p.method
|| !p.key.is_specific_static_name("__proto__") =>
{
new_properties.push(prop);
}
ObjectPropertyKind::ObjectProperty(p) => {
ctx.drop_expression(&p.value);
}
}
}
}
e if should_fold_spread_element(e, ctx) => {
ctx.drop_expression(&spread_element.argument);
}
_ => {
new_properties.push(ObjectPropertyKind::SpreadProperty(spread_element));
}
}
} else {
new_properties.push(p);
}
}
e.properties = new_properties;
ctx.notice_change();
}
fn is_spread_inlineable_object_literal(
e: &ObjectExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
e.properties.iter().all(|p| match p {
ObjectPropertyKind::SpreadProperty(_) => true,
ObjectPropertyKind::ObjectProperty(p) => {
matches!(p.kind, PropertyKind::Init)
&& (
p.computed
|| p.method
|| !p.key.is_specific_static_name("__proto__")
|| !p.value.may_have_side_effects(ctx)
)
}
})
}
pub fn inline_template_literal(t: &mut TemplateLiteral<'a>, ctx: &mut TraverseCtx<'a>) {
let mut inline_exprs = Vec::new();
for (idx, expr) in t.expressions.iter().enumerate() {
if !expr.may_have_side_effects(ctx)
&& let Some(str) = expr.to_js_string(ctx)
{
inline_exprs.push((idx, str));
}
}
if inline_exprs.is_empty() {
return;
}
let mut kept = ArenaVec::with_capacity_in(t.expressions.len() - inline_exprs.len(), ctx);
let mut inline_idxs = inline_exprs.iter().map(|(idx, _)| *idx).peekable();
for (idx, expr) in t.expressions.drain(..).enumerate() {
if inline_idxs.peek() == Some(&idx) {
inline_idxs.next();
ctx.drop_expression(&expr);
} else {
kept.push(expr);
}
}
t.expressions = kept;
for (i, (idx, str)) in inline_exprs.into_iter().enumerate() {
let idx = idx - i;
let next_quasi = (idx + 1 < t.quasis.len()).then(|| t.quasis.remove(idx + 1));
let quasi = &mut t.quasis[idx];
let escaped = Self::escape_string_for_template_literal(&str);
let next_raw = next_quasi.as_ref().map(|q| q.value.raw.as_str()).unwrap_or_default();
quasi.value.raw =
Str::from_strs_array_in([quasi.value.raw.as_str(), &escaped, next_raw], ctx);
let new_cooked = if let (Some(cooked1), Some(cooked2)) =
(quasi.value.cooked, next_quasi.as_ref().map(|q| q.value.cooked))
{
let cooked2_str = cooked2.map(|c| c.as_str()).unwrap_or_default();
Some(Str::from_strs_array_in([cooked1.as_str(), &str, cooked2_str], ctx))
} else {
None
};
quasi.value.cooked = new_cooked;
if next_quasi.is_some_and(|q| q.tail) {
quasi.tail = true;
}
}
}
}
enum ChainFold<'a> {
Unfolded {
has_optional: bool,
},
Flipped {
has_optional: bool,
},
Collapse {
base: Expression<'a>,
base_has_side_effects: bool,
},
}
fn try_fold_chain_at_element<'a>(
elem: &mut ChainElement<'a>,
ctx: &TraverseCtx<'a>,
) -> ChainFold<'a> {
match elem {
ChainElement::CallExpression(c) => try_fold_call_expression(c, ctx),
match_member_expression!(ChainElement) => {
try_fold_member_expression(elem.to_member_expression_mut(), ctx)
}
ChainElement::TSNonNullExpression(t) => try_fold_chain_at_expr(&mut t.expression, ctx),
}
}
fn try_fold_chain_at_expr<'a>(expr: &mut Expression<'a>, ctx: &TraverseCtx<'a>) -> ChainFold<'a> {
match expr.get_inner_expression_mut() {
Expression::CallExpression(c) => try_fold_call_expression(c, ctx),
match_member_expression!(Expression) => {
try_fold_member_expression(expr.to_member_expression_mut(), ctx)
}
_ => ChainFold::Unfolded { has_optional: false },
}
}
fn try_fold_call_expression<'a>(
call: &mut CallExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> ChainFold<'a> {
match try_fold_chain_at_expr(&mut call.callee, ctx) {
ChainFold::Flipped { has_optional } => {
ChainFold::Flipped { has_optional: has_optional || call.optional }
}
collapse @ ChainFold::Collapse { .. } => collapse,
ChainFold::Unfolded { has_optional } => {
try_fold_at_optional(&mut call.optional, &mut call.callee, has_optional, ctx)
.unwrap_or(ChainFold::Unfolded { has_optional: has_optional || call.optional })
}
}
}
fn try_fold_member_expression<'a>(
member: &mut MemberExpression<'a>,
ctx: &TraverseCtx<'a>,
) -> ChainFold<'a> {
let (optional_mut, object) = member_expression_optional_and_object_mut(member);
let optional = *optional_mut;
match try_fold_chain_at_expr(object, ctx) {
ChainFold::Flipped { has_optional } => {
ChainFold::Flipped { has_optional: has_optional || optional }
}
collapse @ ChainFold::Collapse { .. } => collapse,
ChainFold::Unfolded { has_optional } => {
try_fold_at_optional(optional_mut, object, has_optional, ctx)
.unwrap_or(ChainFold::Unfolded { has_optional: has_optional || optional })
}
}
}
fn member_expression_optional_and_object_mut<'a, 'b>(
member: &'b mut MemberExpression<'a>,
) -> (&'b mut bool, &'b mut Expression<'a>) {
match member {
MemberExpression::StaticMemberExpression(m) => {
let m = &mut **m;
(&mut m.optional, &mut m.object)
}
MemberExpression::ComputedMemberExpression(m) => {
let m = &mut **m;
(&mut m.optional, &mut m.object)
}
MemberExpression::PrivateFieldExpression(m) => {
let m = &mut **m;
(&mut m.optional, &mut m.object)
}
}
}
fn try_fold_at_optional<'a>(
optional: &mut bool,
base: &mut Expression<'a>,
has_optional: bool,
ctx: &TraverseCtx<'a>,
) -> Option<ChainFold<'a>> {
if !*optional || has_optional {
return None;
}
match base.value_type(ctx) {
ValueType::Null | ValueType::Undefined => {
let base_has_side_effects = base.may_have_side_effects(ctx);
let taken = base.take_in(ctx);
Some(ChainFold::Collapse { base: taken, base_has_side_effects })
}
ValueType::Undetermined => None,
_ => {
*optional = false;
Some(ChainFold::Flipped { has_optional: false })
}
}
}
pub(super) fn is_cjs_module_exports_hint(expr: &Expression<'_>) -> bool {
let Expression::AssignmentExpression(assign) = expr.get_inner_expression() else {
return false;
};
assign.operator == AssignmentOperator::Assign
&& assign
.left
.as_member_expression()
.is_some_and(|m| m.is_specific_member_access("module", "exports"))
}