mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
feat(minifier): constant fold nullish coalescing operator (#5761)
This commit is contained in:
parent
77d9170f84
commit
e968e9ffd0
3 changed files with 110 additions and 1 deletions
|
|
@ -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<Expression<'a>> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue