diff --git a/crates/oxc_minifier/src/ast_passes/fold_constants.rs b/crates/oxc_minifier/src/ast_passes/fold_constants.rs index 646202a77d062..9b12f946e41dc 100644 --- a/crates/oxc_minifier/src/ast_passes/fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/fold_constants.rs @@ -10,7 +10,9 @@ use oxc_syntax::{ use oxc_traverse::{Ancestor, Traverse, TraverseCtx}; use crate::{ - node_util::{is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil, NumberValue}, + node_util::{ + is_exact_int64, IsLiteralValue, MayHaveSideEffects, NodeUtil, NumberValue, ValueType, + }, tri::Tri, ty::Ty, CompressorPass, @@ -51,6 +53,9 @@ impl<'a> FoldConstants { { self.try_fold_and_or(e, ctx) } + Expression::LogicalExpression(e) if e.operator == LogicalOperator::Coalesce => { + self.try_fold_coalesce(e, ctx) + } Expression::UnaryExpression(e) => self.try_fold_unary_expression(e, ctx), _ => None, } { @@ -655,4 +660,39 @@ impl<'a> FoldConstants { } None } + + /// Try to fold a nullish coalesce `foo ?? bar`. + pub fn try_fold_coalesce( + &self, + logical_expr: &mut LogicalExpression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option> { + debug_assert_eq!(logical_expr.operator, LogicalOperator::Coalesce); + let left = &logical_expr.left; + let left_val = ctx.get_known_value_type(left); + match left_val { + ValueType::Null | ValueType::Void => { + Some(if left.may_have_side_effects() { + // e.g. `(a(), null) ?? 1` => `(a(), null, 1)` + let expressions = ctx.ast.vec_from_iter([ + ctx.ast.move_expression(&mut logical_expr.left), + ctx.ast.move_expression(&mut logical_expr.right), + ]); + ctx.ast.expression_sequence(SPAN, expressions) + } else { + // nullish condition => this expression evaluates to the right side. + ctx.ast.move_expression(&mut logical_expr.right) + }) + } + ValueType::Number + | ValueType::Bigint + | ValueType::String + | ValueType::Boolean + | ValueType::Object => { + // non-nullish condition => this expression evaluates to the left side. + Some(ctx.ast.move_expression(&mut logical_expr.left)) + } + ValueType::Undetermined => None, + } + } } diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs index c7ee550153a32..8ccfdb5b8ae9c 100644 --- a/crates/oxc_minifier/src/node_util/mod.rs +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -22,6 +22,18 @@ pub fn is_exact_int64(num: f64) -> bool { num.fract() == 0.0 } +#[derive(Debug, Eq, PartialEq)] +pub enum ValueType { + Undetermined, + Null, + Void, + Number, + Bigint, + String, + Boolean, + Object, +} + pub trait NodeUtil { fn symbols(&self) -> &SymbolTable; @@ -391,4 +403,27 @@ pub trait NodeUtil { return BigInt::parse_bytes(s.as_bytes(), 10); } + + /// Evaluate and attempt to determine which primitive value type it could resolve to. + /// Without proper type information some assumptions had to be made for operations that could + /// result in a BigInt or a Number. If there is not enough information available to determine one + /// or the other then we assume Number in order to maintain historical behavior of the compiler and + /// avoid breaking projects that relied on this behavior. + fn get_known_value_type(&self, e: &Expression<'_>) -> ValueType { + match e { + Expression::NumericLiteral(_) => ValueType::Number, + Expression::NullLiteral(_) => ValueType::Null, + Expression::ArrayExpression(_) | Expression::ObjectExpression(_) => ValueType::Object, + Expression::BooleanLiteral(_) => ValueType::Boolean, + Expression::Identifier(ident) if self.is_identifier_undefined(ident) => ValueType::Void, + Expression::SequenceExpression(e) => e + .expressions + .last() + .map_or(ValueType::Undetermined, |e| self.get_known_value_type(e)), + Expression::BigIntLiteral(_) => ValueType::Bigint, + Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => ValueType::String, + // TODO: complete this + _ => ValueType::Undetermined, + } + } } diff --git a/crates/oxc_minifier/tests/ast_passes/fold_constants.rs b/crates/oxc_minifier/tests/ast_passes/fold_constants.rs index 1a87a94c3b310..035f0bd3208c0 100644 --- a/crates/oxc_minifier/tests/ast_passes/fold_constants.rs +++ b/crates/oxc_minifier/tests/ast_passes/fold_constants.rs @@ -605,6 +605,40 @@ fn test_fold_logical_op2() { test("x = [(function(){alert(x)})()] && x", "x=([function(){alert(x)}()],x)"); } +#[test] +fn test_fold_nullish_coalesce() { + // fold if left is null/undefined + test("null ?? 1", "1"); + test("undefined ?? false", "false"); + test("(a(), null) ?? 1", "(a(), null, 1)"); + + test("x = [foo()] ?? x", "x = [foo()]"); + + // short circuit on all non nullish LHS + test("x = false ?? x", "x = false"); + test("x = true ?? x", "x = true"); + test("x = 0 ?? x", "x = 0"); + test("x = 3 ?? x", "x = 3"); + + // unfoldable, because the right-side may be the result + test_same("a = x ?? true"); + test_same("a = x ?? false"); + test_same("a = x ?? 3"); + test_same("a = b ? c : x ?? false"); + test_same("a = b ? x ?? false : c"); + + // folded, but not here. + test_same("a = x ?? false ? b : c"); + test_same("a = x ?? true ? b : c"); + + test_same("x = foo() ?? true ?? bar()"); + test("x = foo() ?? (true && bar())", "x = foo() ?? bar()"); + test_same("x = (foo() || false) ?? bar()"); + + test("a() ?? (1 ?? b())", "a() ?? 1"); + test("(a() ?? 1) ?? b()", "a() ?? 1 ?? b()"); +} + #[test] fn test_fold_void() { test_same("void 0");