diff --git a/crates/oxc_minifier/src/peephole/fold_constants.rs b/crates/oxc_minifier/src/peephole/fold_constants.rs index 52703b7525ee9..03007b154b403 100644 --- a/crates/oxc_minifier/src/peephole/fold_constants.rs +++ b/crates/oxc_minifier/src/peephole/fold_constants.rs @@ -1660,8 +1660,8 @@ mod test { #[test] fn test_fold_left() { - fold_same("(+x - 1) + 2"); // not yet - fold("(+x & 1) & 2", "+x & 0"); + fold("(+x - 1) + 2", "x - 1 + 2"); // not yet + fold("(+x & 1) & 2", "x & 0"); } #[test] diff --git a/crates/oxc_minifier/src/peephole/replace_known_methods.rs b/crates/oxc_minifier/src/peephole/replace_known_methods.rs index bc61cd94af63b..488d1325c255b 100644 --- a/crates/oxc_minifier/src/peephole/replace_known_methods.rs +++ b/crates/oxc_minifier/src/peephole/replace_known_methods.rs @@ -1816,11 +1816,11 @@ mod test { #[test] fn test_fold_pow() { test("Math.pow(2, 3)", "2 ** 3"); - test("Math.pow(a, 3)", "+(a) ** 3"); - test("Math.pow(2, b)", "2 ** +b"); - test("Math.pow(a, b)", "+(a) ** +b"); - test("Math.pow(2n, 3n)", "+(2n) ** +3n"); // errors both before and after - test("Math.pow(a + b, c)", "+(a + b) ** +c"); + test("Math.pow(a, 3)", "a ** 3"); + test("Math.pow(2, b)", "2 ** b"); + test("Math.pow(a, b)", "a ** +b"); + test("Math.pow(2n, 3n)", "2n ** +3n"); // errors both before and after + test("Math.pow(a + b, c)", "(a + b) ** +c"); test_same("Math.pow()"); test_same("Math.pow(1)"); test_same("Math.pow(...a, 1)"); diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index fadfd1c9bd7f9..490d259674476 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -104,6 +104,7 @@ impl<'a> PeepholeOptimizations { Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx), Expression::BinaryExpression(e) => Self::try_fold_loose_equals_undefined(e, ctx) .or_else(|| Self::try_compress_typeof_undefined(e, ctx)), + Expression::UnaryExpression(e) => Self::try_remove_unary_plus(e, ctx), Expression::NewExpression(e) => Self::get_fold_constructor_name(&e.callee, ctx) .and_then(|name| { Self::try_fold_object_or_array_constructor(e.span, name, &mut e.arguments, ctx) @@ -200,6 +201,78 @@ impl<'a> PeepholeOptimizations { Some(ctx.ast.expression_binary(expr.span, unary_expr.unbox().argument, new_eq_op, right)) } + /// Remove unary `+` if `ToNumber` conversion is done by the parent expression + /// + /// - `1 - +b` => `1 - b` (for other operators as well) + /// - `+a - 1` => `a - 1` (for other operators as well) + fn try_remove_unary_plus( + expr: &mut UnaryExpression<'a>, + ctx: Ctx<'a, '_>, + ) -> Option> { + if expr.operator != UnaryOperator::UnaryPlus { + return None; + } + + let parent_expression = ctx.ancestors().next()?; + let parent_expression_does_to_number_conversion = match parent_expression { + Ancestor::BinaryExpressionLeft(e) => { + Self::is_binary_operator_that_does_number_conversion(*e.operator()) + && ValueType::from(e.right()).is_number() + } + Ancestor::BinaryExpressionRight(e) => { + Self::is_binary_operator_that_does_number_conversion(*e.operator()) + && ValueType::from(e.left()).is_number() + } + _ => false, + }; + if !parent_expression_does_to_number_conversion { + return None; + } + + Some(ctx.ast.move_expression(&mut expr.argument)) + } + + /// For `+a - n` => `a - n` (assuming n is a number) + /// + /// Before compression the evaluation is: + /// 1. `a_2 = ToNumber(a)` + /// 2. `a_3 = ToNumeric(a_2)` + /// 3. `n_2 = ToNumeric(n)` (no-op since n is a number) + /// 4. If the type of `a_3` is not number, throw an error + /// 5. Calculate the result of the binary operation + /// + /// After compression, step 1 is removed. The difference we need to care is + /// the difference with `ToNumber(a)` and `ToNumeric(a)` because `ToNumeric(a_2)` is a no-op. + /// + /// - When `a` is an object and `ToPrimitive(a, NUMBER)` returns a BigInt, + /// - `ToNumeric(a)` will return that value. But the binary operation will throw an error in step 4. + /// - `ToNumber(a)` will throw an error. + /// - When `a` is an object and `ToPrimitive(a, NUMBER)` returns a value other than BigInt, + /// `ToNumeric(a)` and `ToNumber(a)` works the same. Because the step 2 in `ToNumeric` is always `false`. + /// - When `a` is BigInt, + /// - `ToNumeric(a)` will return that value. But the binary operation will throw an error in step 4. + /// - `ToNumber(a)` will throw an error. + /// - When `a` is not a object nor a BigInt, `ToNumeric(a)` and `ToNumber(a)` works the same. + /// Because the step 2 in `ToNumeric` is always `false`. + /// + /// Thus, removing `+` is fine. + fn is_binary_operator_that_does_number_conversion(operator: BinaryOperator) -> bool { + matches!( + operator, + BinaryOperator::Exponential + | BinaryOperator::Multiplication + | BinaryOperator::Division + | BinaryOperator::Remainder + | BinaryOperator::Subtraction + | BinaryOperator::ShiftLeft + | BinaryOperator::ShiftRight + | BinaryOperator::ShiftRightZeroFill + | BinaryOperator::BitwiseAnd + | BinaryOperator::BitwiseXOR + | BinaryOperator::BitwiseOR + ) + } + /// `a || (b || c);` -> `(a || b) || c;` fn try_rotate_logical_expression( expr: &mut LogicalExpression<'a>, @@ -1480,6 +1553,17 @@ mod test { test_same("var foo; v = typeof foo == 'string' && foo !== null"); } + #[test] + fn test_remove_unary_plus() { + test("v = 1 - +foo", "v = 1 - foo"); + test("v = +foo - 1", "v = foo - 1"); + test_same("v = 1n - +foo"); + test_same("v = +foo - 1n"); + test_same("v = +foo - bar"); + test_same("v = foo - +bar"); + test_same("v = 1 + +foo"); // cannot compress into `1 + foo` because `foo` can be a string + } + #[test] fn test_fold_loose_equals_undefined() { test_same("foo != null"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 622cc34e0b8bc..f454f95bfd91b 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,7 +3,7 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.51 kB | 23.70 kB | 8.47 kB | 8.54 kB | react.development.js -173.90 kB | 59.55 kB | 59.82 kB | 19.18 kB | 19.33 kB | moment.js +173.90 kB | 59.54 kB | 59.82 kB | 19.18 kB | 19.33 kB | moment.js 287.63 kB | 89.46 kB | 90.07 kB | 30.97 kB | 31.95 kB | jquery.js @@ -11,17 +11,17 @@ Original | minified | minified | gzip | gzip | Fixture 544.10 kB | 71.40 kB | 72.48 kB | 25.86 kB | 26.20 kB | lodash.js -555.77 kB | 271.21 kB | 270.13 kB | 88.30 kB | 90.80 kB | d3.js +555.77 kB | 271.12 kB | 270.13 kB | 88.26 kB | 90.80 kB | d3.js 1.01 MB | 440.94 kB | 458.89 kB | 122.52 kB | 126.71 kB | bundle.min.js -1.25 MB | 650.35 kB | 646.76 kB | 160.96 kB | 163.73 kB | three.js +1.25 MB | 650.30 kB | 646.76 kB | 160.95 kB | 163.73 kB | three.js -2.14 MB | 717.10 kB | 724.14 kB | 162.05 kB | 181.07 kB | victory.js +2.14 MB | 716.93 kB | 724.14 kB | 161.98 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 324.35 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 324.33 kB | 331.56 kB | echarts.js 6.69 MB | 2.28 MB | 2.31 MB | 467.77 kB | 488.28 kB | antd.js -10.95 MB | 3.36 MB | 3.49 MB | 861.81 kB | 915.50 kB | typescript.js +10.95 MB | 3.36 MB | 3.49 MB | 861.79 kB | 915.50 kB | typescript.js