diff --git a/crates/oxc_ast/src/ast_impl/js.rs b/crates/oxc_ast/src/ast_impl/js.rs index 21e7ccb1c470a..095e9bc437896 100644 --- a/crates/oxc_ast/src/ast_impl/js.rs +++ b/crates/oxc_ast/src/ast_impl/js.rs @@ -263,6 +263,18 @@ impl<'a> Expression<'a> { matches!(self, Expression::FunctionExpression(_) | Expression::ArrowFunctionExpression(_)) } + /// Returns `true` if this [`Expression`] is an anonymous function definition. + /// Note that this includes [`Class`]s. + /// + pub fn is_anonymous_function_definition(&self) -> bool { + match self { + Self::ArrowFunctionExpression(_) => true, + Self::FunctionExpression(func) => func.name().is_none(), + Self::ClassExpression(class) => class.name().is_none(), + _ => false, + } + } + /// Returns `true` if this [`Expression`] is a [`CallExpression`]. pub fn is_call_expression(&self) -> bool { matches!(self, Expression::CallExpression(_)) @@ -1160,7 +1172,13 @@ impl<'a> ArrowFunctionExpression<'a> { } } -impl Class<'_> { +impl<'a> Class<'a> { + /// Returns this [`Class`]'s name, if it has one. + #[inline] + pub fn name(&self) -> Option> { + self.id.as_ref().map(|id| id.name.clone()) + } + /// `true` if this [`Class`] is an expression. /// /// For example, diff --git a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs index 8d5ebf3ff5e05..469d246895783 100644 --- a/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs +++ b/crates/oxc_minifier/src/ast_passes/peephole_substitute_alternate_syntax.rs @@ -4,6 +4,7 @@ use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ValueType}, ToInt32, ToJsString, ToNumber, }; +use oxc_span::cmp::ContentEq; use oxc_span::{GetSpan, SPAN}; use oxc_syntax::{ es_target::ESTarget, @@ -153,6 +154,9 @@ impl<'a> Traverse<'a> for PeepholeSubstituteAlternateSyntax { 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::ConditionalExpression(e) => { + Self::try_merge_conditional_expression_inside(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) @@ -623,6 +627,46 @@ impl<'a, 'b> PeepholeSubstituteAlternateSyntax { } } + /// Merge `consequent` and `alternate` of `ConditionalExpression` inside. + /// + /// - `x ? a = 0 : a = 1` -> `a = x ? 0 : 1` + fn try_merge_conditional_expression_inside( + expr: &mut ConditionalExpression<'a>, + ctx: Ctx<'a, 'b>, + ) -> Option> { + let ( + Expression::AssignmentExpression(consequent), + Expression::AssignmentExpression(alternate), + ) = (&mut expr.consequent, &mut expr.alternate) + else { + return None; + }; + if !matches!(consequent.left, AssignmentTarget::AssignmentTargetIdentifier(_)) { + return None; + } + if consequent.right.is_anonymous_function_definition() { + return None; + } + if consequent.operator != AssignmentOperator::Assign + || consequent.operator != alternate.operator + || consequent.left.content_ne(&alternate.left) + { + return None; + } + + Some(ctx.ast.expression_assignment( + SPAN, + consequent.operator, + ctx.ast.move_assignment_target(&mut alternate.left), + ctx.ast.expression_conditional( + SPAN, + ctx.ast.move_expression(&mut expr.test), + ctx.ast.move_expression(&mut consequent.right), + ctx.ast.move_expression(&mut alternate.right), + ), + )) + } + /// Fold `Boolean`, `Number`, `String`, `BigInt` constructors. /// /// `Boolean(a)` -> `!!a` @@ -1125,6 +1169,28 @@ mod test { test_same("x += -1"); } + #[test] + fn test_compress_conditional_expression_inside() { + test("x ? a = 0 : a = 1", "a = x ? 0 : 1"); + test( + "x ? a = function foo() { return 'a' } : a = function bar() { return 'b' }", + "a = x ? function foo() { return 'a' } : function bar() { return 'b' }", + ); + + // a.b might have a side effect + test_same("x ? a.b = 0 : a.b = 1"); + // `a = x ? () => 'a' : () => 'b'` does not set the name property of the function + test_same("x ? a = () => 'a' : a = () => 'b'"); + test_same("x ? a = function () { return 'a' } : a = function () { return 'b' }"); + test_same("x ? a = class { foo = 'a' } : a = class { foo = 'b' }"); + + // for non `=` operators, `GetValue(lref)` is called before `Evaluation of AssignmentExpression` + // so cannot be fold to `a += x ? 0 : 1` + // example case: `(()=>{"use strict"; (console.log("log"), 1) ? a += 0 : a += 1; })()` + test_same("x ? a += 0 : a += 1"); + test_same("x ? a &&= 0 : a &&= 1"); + } + #[test] fn test_fold_literal_object_constructors() { test("x = new Object", "x = ({})"); diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 1ab509dc6d1b3..c4958f8ba25d2 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -3,25 +3,25 @@ Original | minified | minified | gzip | gzip | Fixture ------------------------------------------------------------------------------------- 72.14 kB | 23.68 kB | 23.70 kB | 8.61 kB | 8.54 kB | react.development.js -173.90 kB | 59.78 kB | 59.82 kB | 19.42 kB | 19.33 kB | moment.js +173.90 kB | 59.77 kB | 59.82 kB | 19.41 kB | 19.33 kB | moment.js -287.63 kB | 90.04 kB | 90.07 kB | 32.06 kB | 31.95 kB | jquery.js +287.63 kB | 90.02 kB | 90.07 kB | 32.05 kB | 31.95 kB | jquery.js -342.15 kB | 118.14 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js +342.15 kB | 118.12 kB | 118.14 kB | 44.51 kB | 44.37 kB | vue.js -544.10 kB | 71.75 kB | 72.48 kB | 26.16 kB | 26.20 kB | lodash.js +544.10 kB | 71.72 kB | 72.48 kB | 26.15 kB | 26.20 kB | lodash.js -555.77 kB | 272.82 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js +555.77 kB | 272.81 kB | 270.13 kB | 90.92 kB | 90.80 kB | d3.js -1.01 MB | 460.22 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js +1.01 MB | 460.21 kB | 458.89 kB | 126.83 kB | 126.71 kB | bundle.min.js -1.25 MB | 652.56 kB | 646.76 kB | 163.52 kB | 163.73 kB | three.js +1.25 MB | 652.53 kB | 646.76 kB | 163.51 kB | 163.73 kB | three.js -2.14 MB | 726.04 kB | 724.14 kB | 180.15 kB | 181.07 kB | victory.js +2.14 MB | 726 kB | 724.14 kB | 180.13 kB | 181.07 kB | victory.js -3.20 MB | 1.01 MB | 1.01 MB | 331.83 kB | 331.56 kB | echarts.js +3.20 MB | 1.01 MB | 1.01 MB | 331.78 kB | 331.56 kB | echarts.js -6.69 MB | 2.32 MB | 2.31 MB | 492.72 kB | 488.28 kB | antd.js +6.69 MB | 2.32 MB | 2.31 MB | 492.61 kB | 488.28 kB | antd.js -10.95 MB | 3.50 MB | 3.49 MB | 908.28 kB | 915.50 kB | typescript.js +10.95 MB | 3.50 MB | 3.49 MB | 908.24 kB | 915.50 kB | typescript.js