mirror of
https://github.com/danbulant/oxc
synced 2026-05-24 12:21:58 +00:00
feat(minifier): fold string number comparison (#538)
This commit is contained in:
parent
357b28eb96
commit
f0495ef924
3 changed files with 145 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Reference in a new issue