From f0495ef924a78ee80c3eb16f540843a45b854d6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=98=BF=E8=89=AF=E4=BB=94?= <32487868+cijiugechu@users.noreply.github.com> Date: Tue, 11 Jul 2023 21:50:51 +0800 Subject: [PATCH] feat(minifier): fold string number comparison (#538) --- crates/oxc_hir/src/hir_util.rs | 20 +++- crates/oxc_minifier/src/compressor/fold.rs | 97 ++++++++++++++++++- .../tests/closure/fold_constants.rs | 32 ++++++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/crates/oxc_hir/src/hir_util.rs b/crates/oxc_hir/src/hir_util.rs index bb11c5af1..848da8c02 100644 --- a/crates/oxc_hir/src/hir_util.rs +++ b/crates/oxc_hir/src/hir_util.rs @@ -6,8 +6,8 @@ use oxc_semantic::ReferenceFlag; use oxc_syntax::operator::{AssignmentOperator, LogicalOperator, UnaryOperator}; use crate::hir::{ - ArrayExpressionElement, Expression, NumberLiteral, ObjectProperty, ObjectPropertyKind, - PropertyKey, SpreadElement, UnaryExpression, + ArrayExpressionElement, BinaryExpression, Expression, NumberLiteral, ObjectProperty, + ObjectPropertyKind, PropertyKey, SpreadElement, UnaryExpression, }; /// Code ported from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/NodeUtil.java#LL836C6-L836C6) @@ -122,6 +122,9 @@ impl<'a, 'b> CheckForStateChange<'a, 'b> for Expression<'a> { Self::UnaryExpression(unary_expr) => { unary_expr.check_for_state_change(check_for_new_objects) } + Self::BinaryExpression(binary_expr) => { + binary_expr.check_for_state_change(check_for_new_objects) + } Self::ObjectExpression(object_expr) => { if check_for_new_objects { return true; @@ -155,6 +158,15 @@ impl<'a, 'b> CheckForStateChange<'a, 'b> for UnaryExpression<'a> { } } +impl<'a, 'b> CheckForStateChange<'a, 'b> for BinaryExpression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + let left = self.left.check_for_state_change(check_for_new_objects); + let right = self.right.check_for_state_change(check_for_new_objects); + + left || right + } +} + impl<'a, 'b> CheckForStateChange<'a, 'b> for ArrayExpressionElement<'a> { fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { match self { @@ -227,6 +239,10 @@ impl NumberValue { Self::NaN => Self::NaN, } } + + pub fn is_nan(&self) -> bool { + matches!(self, Self::NaN) + } } pub fn is_exact_int64(num: f64) -> bool { diff --git a/crates/oxc_minifier/src/compressor/fold.rs b/crates/oxc_minifier/src/compressor/fold.rs index 909a104a4..d127934e0 100644 --- a/crates/oxc_minifier/src/compressor/fold.rs +++ b/crates/oxc_minifier/src/compressor/fold.rs @@ -141,10 +141,31 @@ impl<'a> From<&Expression<'a>> for Ty { } Self::Number } + UnaryOperator::UnaryPlus => Self::Number, UnaryOperator::LogicalNot => Self::Boolean, UnaryOperator::Typeof => Self::Str, _ => Self::Undetermined, }, + Expression::BinaryExpression(binary_expr) => match binary_expr.operator { + BinaryOperator::Addition => { + let left_ty = Self::from(&binary_expr.left); + let right_ty = Self::from(&binary_expr.right); + + if left_ty == Self::Str || right_ty == Self::Str { + return Self::Str; + } + + // There are some pretty weird cases for object types: + // {} + [] === "0" + // [] + {} === "[object Object]" + if left_ty == Self::Object || right_ty == Self::Object { + return Self::Undetermined; + } + + Self::Undetermined + } + _ => Self::Undetermined, + }, _ => Self::Undetermined, } } @@ -222,7 +243,7 @@ impl<'a> Compressor<'a> { } fn evaluate_comparison<'b>( - &self, + &mut self, op: BinaryOperator, left: &'b Expression<'a>, right: &'b Expression<'a>, @@ -254,7 +275,7 @@ impl<'a> Compressor<'a> { /// fn try_abstract_equality_comparison<'b>( - &self, + &mut self, left_expr: &'b Expression<'a>, right_expr: &'b Expression<'a>, ) -> Tri { @@ -268,6 +289,56 @@ impl<'a> Compressor<'a> { return Tri::True; } + if matches!((left, right), (Ty::Number, Ty::Str)) || matches!(right, Ty::Boolean) { + let right_number = get_side_free_number_value(right_expr); + + if let Some(r_num) = right_number && + let NumberValue::Number(num) = r_num { + let raw = self.hir.new_str(num.to_string().as_str()); + + let number_literal = self.hir.number_literal( + right_expr.span(), + num, + raw, + if num.fract() == 0.0 { + NumberBase::Decimal + } else { + NumberBase::Float + } + ); + let number_literal_expr = self.hir.literal_number_expression(number_literal); + + return self.try_abstract_equality_comparison(left_expr, &number_literal_expr); + } + + return Tri::Unknown; + } + + if matches!((left, right), (Ty::Str, Ty::Number)) || matches!(left, Ty::Boolean) { + let left_number = get_side_free_number_value(left_expr); + + if let Some(l_num) = left_number && + let NumberValue::Number(num) = l_num { + let raw = self.hir.new_str(num.to_string().as_str()); + + let number_literal = self.hir.number_literal( + left_expr.span(), + num, + raw, + if num.fract() == 0.0 { + NumberBase::Decimal + } else { + NumberBase::Float + } + ); + let number_literal_expr = self.hir.literal_number_expression(number_literal); + + return self.try_abstract_equality_comparison(&number_literal_expr, right_expr); + } + + return Tri::Unknown; + } + if matches!(left, Ty::BigInt) || matches!(right, Ty::BigInt) { let left_bigint = get_side_free_bigint_value(left_expr); let right_bigint = get_side_free_bigint_value(right_expr); @@ -277,6 +348,14 @@ impl<'a> Compressor<'a> { } } + if matches!(left, Ty::Str | Ty::Number) && matches!(right, Ty::Object) { + return Tri::Unknown; + } + + if matches!(left, Ty::Object) && matches!(right, Ty::Str | Ty::Number) { + return Tri::Unknown; + } + return Tri::False; } Tri::Unknown @@ -362,6 +441,20 @@ impl<'a> Compressor<'a> { return Tri::False; } return match left { + Ty::Number => { + let left_number = get_side_free_number_value(left_expr); + let right_number = get_side_free_number_value(right_expr); + + if let Some(l_num) = left_number && let Some(r_num) = right_number { + if l_num.is_nan() || r_num.is_nan() { + return Tri::False; + } + + return Tri::for_boolean(l_num == r_num); + } + + Tri::Unknown + } Ty::Str => { let left_string = get_side_free_string_value(left_expr); let right_string = get_side_free_string_value(right_expr); diff --git a/crates/oxc_minifier/tests/closure/fold_constants.rs b/crates/oxc_minifier/tests/closure/fold_constants.rs index a58bbfeb5..71524e91d 100644 --- a/crates/oxc_minifier/tests/closure/fold_constants.rs +++ b/crates/oxc_minifier/tests/closure/fold_constants.rs @@ -153,6 +153,38 @@ fn test_string_string_comparison() { test_same("''+x===''+x"); // potentially foldable } +#[test] +fn test_number_string_comparison() { + test_wcb("1 < '2'", "true"); + test_wcb("2 > '1'", "true"); + test_wcb("123 > '34'", "true"); + test_wcb("NaN >= 'NaN'", "false"); + test_wcb("1 == '2'", "false"); + test_wcb("1 != '1'", "false"); + test_wcb("NaN == 'NaN'", "false"); + test_wcb("1 === '1'", "false"); + test_wcb("1 !== '1'", "true"); + test_same("+x>''+y"); + test_same("+x==''+y"); + test_wcb("+x !== '' + y", "true"); +} + +#[test] +fn test_string_number_comparison() { + test_wcb("'1' < 2", "true"); + test_wcb("'2' > 1", "true"); + test_wcb("'123' > 34", "true"); + test_wcb("'NaN' < NaN", "false"); + test_wcb("'1' == 2", "false"); + test_wcb("'1' != 1", "false"); + test_wcb("'NaN' == NaN", "false"); + test_wcb("'1' === 1", "false"); + test_wcb("'1' !== 1", "true"); + test_same("''+x<+y"); + test_same("''+x==+y"); + test_wcb("'' + x === +y", "false"); +} + #[test] fn test_bigint_number_comparison() { test_wcb("1n < 2", "true");