feat(minifier): fold string number comparison (#538)

This commit is contained in:
阿良仔 2023-07-11 21:50:51 +08:00 committed by GitHub
parent 357b28eb96
commit f0495ef924
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 145 additions and 4 deletions

View file

@ -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 {

View file

@ -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> {
/// <https://tc39.es/ecma262/#sec-abstract-equality-comparison>
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);

View file

@ -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");