diff --git a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs index 4272c04d675bc..7fb330abce15e 100644 --- a/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/peephole/substitute_alternate_syntax.rs @@ -7,6 +7,7 @@ use oxc_ecmascript::{ }; use oxc_span::cmp::ContentEq; use oxc_span::GetSpan; +use oxc_span::SPAN; use oxc_syntax::{ es_target::ESTarget, identifier::is_identifier_name, @@ -134,6 +135,7 @@ impl<'a, 'b> PeepholeOptimizations { Self::try_compress_assignment_to_update_expression(e, ctx) } Expression::LogicalExpression(e) => Self::try_compress_is_null_or_undefined(e, ctx) + .or_else(|| Self::try_compress_is_object_and_not_null(e, ctx)) .or_else(|| self.try_compress_logical_expression_to_assignment_expression(e, ctx)) .or_else(|| Self::try_rotate_logical_expression(e, ctx)), Expression::TemplateLiteral(t) => Self::try_fold_template_literal(t, ctx), @@ -576,6 +578,171 @@ impl<'a, 'b> PeepholeOptimizations { } } + /// Compress `typeof foo === 'object' && foo !== null` into `typeof foo == 'object' && !!foo`. + /// + /// - `typeof foo === 'object' && foo !== null` => `typeof foo == 'object' && !!foo` + /// - `typeof foo == 'object' && foo != null` => `typeof foo == 'object' && !!foo` + /// - `typeof foo !== 'object' || foo === null` => `typeof foo != 'object' || !foo` + /// - `typeof foo != 'object' || foo == null` => `typeof foo != 'object' || !foo` + /// + /// If `typeof foo == 'object'`, then `foo` is guaranteed to be an object or null. + /// - If `foo` is an object, then `foo !== null` is `true`. If `foo` is null, then `foo !== null` is `false`. + /// - If `foo` is an object, then `foo != null` is `true`. If `foo` is null, then `foo != null` is `false`. + /// - If `foo` is an object, then `!!foo` is `true`. If `foo` is null, then `!!foo` is `false`. + /// + /// This compression is safe for `document.all` because `typeof document.all` is not `'object'`. + fn try_compress_is_object_and_not_null( + expr: &mut LogicalExpression<'a>, + ctx: Ctx<'a, '_>, + ) -> Option> { + let inversed = match expr.operator { + LogicalOperator::And => false, + LogicalOperator::Or => true, + LogicalOperator::Coalesce => return None, + }; + + if let Some(new_expr) = Self::try_compress_is_object_and_not_null_for_left_and_right( + &expr.left, + &expr.right, + expr.span, + ctx, + inversed, + ) { + return Some(new_expr); + } + + let Expression::LogicalExpression(left) = &mut expr.left else { + return None; + }; + let inversed = match expr.operator { + LogicalOperator::And => false, + LogicalOperator::Or => true, + LogicalOperator::Coalesce => return None, + }; + + Self::try_compress_is_object_and_not_null_for_left_and_right( + &left.right, + &expr.right, + Span::new(left.right.span().start, expr.span.end), + ctx, + inversed, + ) + .map(|new_expr| { + ctx.ast.expression_logical( + expr.span, + ctx.ast.move_expression(&mut left.left), + expr.operator, + new_expr, + ) + }) + } + + fn try_compress_is_object_and_not_null_for_left_and_right( + left: &Expression<'a>, + right: &Expression<'a>, + span: Span, + ctx: Ctx<'a, 'b>, + inversed: bool, + ) -> Option> { + let pair = Self::commutative_pair( + (&left, &right), + |a_expr| { + let Expression::BinaryExpression(a) = a_expr else { return None }; + let is_target_ops = if inversed { + matches!( + a.operator, + BinaryOperator::StrictInequality | BinaryOperator::Inequality + ) + } else { + matches!(a.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality) + }; + if !is_target_ops { + return None; + } + let (id, ()) = Self::commutative_pair( + (&a.left, &a.right), + |a_a| { + let Expression::UnaryExpression(a_a) = a_a else { return None }; + if a_a.operator != UnaryOperator::Typeof { + return None; + } + let Expression::Identifier(id) = &a_a.argument else { return None }; + Some(id) + }, + |b| b.is_specific_string_literal("object").then_some(()), + )?; + Some((id, a_expr)) + }, + |b| { + let Expression::BinaryExpression(b) = b else { + return None; + }; + let is_target_ops = if inversed { + matches!(b.operator, BinaryOperator::StrictEquality | BinaryOperator::Equality) + } else { + matches!( + b.operator, + BinaryOperator::StrictInequality | BinaryOperator::Inequality + ) + }; + if !is_target_ops { + return None; + } + let (id, ()) = Self::commutative_pair( + (&b.left, &b.right), + |a_a| { + let Expression::Identifier(id) = a_a else { return None }; + Some(id) + }, + |b| b.is_null().then_some(()), + )?; + Some(id) + }, + ); + let ((typeof_id_ref, typeof_binary_expr), is_null_id_ref) = pair?; + if typeof_id_ref.name != is_null_id_ref.name { + return None; + } + // It should also return None when the reference might refer to a reference value created by a with statement + // when the minifier supports with statements + if ctx.is_global_reference(typeof_id_ref) { + return None; + } + + let mut new_left_expr = typeof_binary_expr.clone_in(ctx.ast.allocator); + if let Expression::BinaryExpression(new_left_expr_binary) = &mut new_left_expr { + new_left_expr_binary.operator = + if inversed { BinaryOperator::Inequality } else { BinaryOperator::Equality }; + } else { + unreachable!(); + } + + let new_right_expr = if inversed { + ctx.ast.expression_unary( + SPAN, + UnaryOperator::LogicalNot, + ctx.ast.expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name), + ) + } else { + ctx.ast.expression_unary( + SPAN, + UnaryOperator::LogicalNot, + ctx.ast.expression_unary( + SPAN, + UnaryOperator::LogicalNot, + ctx.ast + .expression_identifier_reference(is_null_id_ref.span, is_null_id_ref.name), + ), + ) + }; + Some(ctx.ast.expression_logical( + span, + new_left_expr, + if inversed { LogicalOperator::Or } else { LogicalOperator::And }, + new_right_expr, + )) + } + fn commutative_pair<'x, A, F, G, RetF: 'x, RetG: 'x>( pair: (&'x A, &'x A), check_a: F, @@ -1838,6 +2005,55 @@ mod test { test_same("(_foo = foo) === void 0 || bar === null"); } + #[test] + fn test_fold_is_object_and_not_null() { + test( + "var foo; v = typeof foo === 'object' && foo !== null", + "var foo; v = typeof foo == 'object' && !!foo", + ); + test( + "var foo; v = typeof foo == 'object' && foo !== null", + "var foo; v = typeof foo == 'object' && !!foo", + ); + test( + "var foo; v = typeof foo === 'object' && foo != null", + "var foo; v = typeof foo == 'object' && !!foo", + ); + test( + "var foo; v = typeof foo == 'object' && foo != null", + "var foo; v = typeof foo == 'object' && !!foo", + ); + test( + "var foo; v = typeof foo !== 'object' || foo === null", + "var foo; v = typeof foo != 'object' || !foo", + ); + test( + "var foo; v = typeof foo != 'object' || foo === null", + "var foo; v = typeof foo != 'object' || !foo", + ); + test( + "var foo; v = typeof foo !== 'object' || foo == null", + "var foo; v = typeof foo != 'object' || !foo", + ); + test( + "var foo; v = typeof foo != 'object' || foo == null", + "var foo; v = typeof foo != 'object' || !foo", + ); + test( + "var foo, bar; v = typeof foo === 'object' && foo !== null && bar !== 1", + "var foo, bar; v = typeof foo == 'object' && !!foo && bar !== 1", + ); + test( + "var foo, bar; v = bar !== 1 && typeof foo === 'object' && foo !== null", + "var foo, bar; v = bar !== 1 && typeof foo == 'object' && !!foo", + ); + test_same("var foo; v = typeof foo.a == 'object' && foo.a !== null"); // cannot be folded because accessing foo.a might have a side effect + test_same("v = foo !== null && typeof foo == 'object'"); // cannot be folded because accessing foo might have a side effect + test_same("v = typeof foo == 'object' && foo !== null"); // cannot be folded because accessing foo might have a side effect + test_same("var foo, bar; v = typeof foo == 'object' && bar !== null"); + test_same("var foo; v = typeof foo == 'string' && foo !== null"); + } + #[test] fn test_fold_logical_expression_to_assignment_expression() { test("x || (x = 3)", "x ||= 3"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 86d7d2af88920..aa2e2ca1d8ebd 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -1,27 +1,27 @@ | Oxc | ESBuild | Oxc | ESBuild | Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- -72.14 kB | 23.70 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js +72.14 kB | 23.67 kB | 23.70 kB | 8.60 kB | 8.54 kB | react.development.js 173.90 kB | 59.79 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js -287.63 kB | 90.08 kB | 90.07 kB | 32.03 kB | 31.95 kB | jquery.js +287.63 kB | 90.08 kB | 90.07 kB | 32.02 kB | 31.95 kB | jquery.js 342.15 kB | 118.19 kB | 118.14 kB | 44.45 kB | 44.37 kB | vue.js -544.10 kB | 71.76 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js +544.10 kB | 71.75 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js -555.77 kB | 272.90 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js +555.77 kB | 272.89 kB | 270.13 kB | 90.90 kB | 90.80 kB | d3.js -1.01 MB | 460.18 kB | 458.89 kB | 126.78 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.18 kB | 458.89 kB | 126.77 kB | 126.71 kB | bundle.min.js 1.25 MB | 652.90 kB | 646.76 kB | 163.54 kB | 163.73 kB | three.js -2.14 MB | 724.06 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js +2.14 MB | 724.01 kB | 724.14 kB | 179.94 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 332.00 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 332.01 kB | 331.56 kB | echarts.js 6.69 MB | 2.31 MB | 2.31 MB | 491.99 kB | 488.28 kB | antd.js -10.95 MB | 3.48 MB | 3.49 MB | 905.39 kB | 915.50 kB | typescript.js +10.95 MB | 3.48 MB | 3.49 MB | 905.37 kB | 915.50 kB | typescript.js