diff --git a/crates/oxc_hir/src/hir_util.rs b/crates/oxc_hir/src/hir_util.rs new file mode 100644 index 000000000..11eb98995 --- /dev/null +++ b/crates/oxc_hir/src/hir_util.rs @@ -0,0 +1,77 @@ +use crate::hir::{ + ArrayExpressionElement, Expression, ObjectProperty, ObjectPropertyKind, PropertyKey, + SpreadElement, +}; + +/// Code ported from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/NodeUtil.java#LL836C6-L836C6) +/// Returns true if this is a literal value. We define a literal value as any node that evaluates +/// to the same thing regardless of when or where it is evaluated. So `/xyz/` and `[3, 5]` are +/// literals, but the name a is not. +/// +/// Function literals do not meet this definition, because they lexically capture variables. For +/// example, if you have `function() { return a; }`. +/// If it is evaluated in a different scope, then it captures a different variable. Even if +/// the function did not read any captured variables directly, it would still fail this definition, +/// because it affects the lifecycle of variables in the enclosing scope. +/// +/// However, a function literal with respect to a particular scope is a literal. +/// If `include_functions` is true, all function expressions will be treated as literals. +pub trait IsLiteralValue<'a, 'b> { + fn is_literal_value(&self, include_functions: bool) -> bool; +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for Expression<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + match self { + Self::FunctionExpression(_) | Self::ArrowExpression(_) => include_functions, + Self::ArrayExpression(expr) => { + expr.elements.iter().all(|element| element.is_literal_value(include_functions)) + } + Self::ObjectExpression(expr) => { + expr.properties.iter().all(|property| property.is_literal_value(include_functions)) + } + _ => self.is_immutable_value(), + } + } +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for ArrayExpressionElement<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + match self { + Self::SpreadElement(element) => element.is_literal_value(include_functions), + Self::Expression(expr) => expr.is_literal_value(include_functions), + Self::Elision(_) => true, + } + } +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for SpreadElement<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + self.argument.is_literal_value(include_functions) + } +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for ObjectPropertyKind<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + match self { + Self::ObjectProperty(method) => method.is_literal_value(include_functions), + Self::SpreadProperty(property) => property.is_literal_value(include_functions), + } + } +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for ObjectProperty<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + self.key.is_literal_value(include_functions) + && self.value.is_literal_value(include_functions) + } +} + +impl<'a, 'b> IsLiteralValue<'a, 'b> for PropertyKey<'a> { + fn is_literal_value(&self, include_functions: bool) -> bool { + match self { + Self::Identifier(_) | Self::PrivateIdentifier(_) => false, + Self::Expression(expr) => expr.is_literal_value(include_functions), + } + } +} diff --git a/crates/oxc_hir/src/lib.rs b/crates/oxc_hir/src/lib.rs index d33ba9041..6f0d6098e 100644 --- a/crates/oxc_hir/src/lib.rs +++ b/crates/oxc_hir/src/lib.rs @@ -5,6 +5,7 @@ mod serialize; pub mod hir; mod hir_builder; +pub mod hir_util; pub mod precedence; mod visit_mut; diff --git a/crates/oxc_minifier/src/compressor/fold.rs b/crates/oxc_minifier/src/compressor/fold.rs index 198c8e5f9..b17149491 100644 --- a/crates/oxc_minifier/src/compressor/fold.rs +++ b/crates/oxc_minifier/src/compressor/fold.rs @@ -4,8 +4,9 @@ #[allow(clippy::wildcard_imports)] use oxc_hir::hir::*; -use oxc_span::Span; -use oxc_syntax::operator::BinaryOperator; +use oxc_hir::hir_util::IsLiteralValue; +use oxc_span::{Atom, Span}; +use oxc_syntax::operator::{BinaryOperator, UnaryOperator}; use super::Compressor; @@ -63,6 +64,12 @@ impl<'a> Compressor<'a> { ), _ => None, }, + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::Typeof => { + self.try_fold_typeof(unary_expr.span, &unary_expr.argument) + } + _ => None, + }, _ => None, }; if let Some(folded_expr) = folded_expr { @@ -136,4 +143,41 @@ impl<'a> Compressor<'a> { } Tri::Unknown } + + /// Folds 'typeof(foo)' if foo is a literal, e.g. + /// typeof("bar") --> "string" + /// typeof(6) --> "number" + fn try_fold_typeof<'b>( + &mut self, + span: Span, + argument: &'b Expression<'a>, + ) -> Option> { + if argument.is_literal_value(true) { + let type_name = match argument { + Expression::FunctionExpression(_) | Expression::ArrowExpression(_) => { + Some("function") + } + Expression::StringLiteral(_) | Expression::TemplateLiteral(_) => Some("string"), + Expression::NumberLiteral(_) => Some("number"), + Expression::BooleanLiteral(_) => Some("boolean"), + Expression::NullLiteral(_) + | Expression::ObjectExpression(_) + | Expression::ArrayExpression(_) => Some("object"), + Expression::Identifier(_) if argument.is_undefined() => Some("undefined"), + Expression::UnaryExpression(unary_expr) + if unary_expr.operator == UnaryOperator::Void => + { + Some("undefined") + } + _ => None, + }; + + if let Some(type_name) = type_name { + let string_literal = self.hir.string_literal(span, Atom::from(type_name)); + return Some(self.hir.literal_string_expression(string_literal)); + } + } + + None + } } diff --git a/crates/oxc_minifier/tests/closure/fold_constants.rs b/crates/oxc_minifier/tests/closure/fold_constants.rs index b55c795d1..771418c01 100644 --- a/crates/oxc_minifier/tests/closure/fold_constants.rs +++ b/crates/oxc_minifier/tests/closure/fold_constants.rs @@ -1,8 +1,27 @@ //! -use crate::test; +use crate::{test, test_same}; #[test] fn undefined_comparison1() { test("undefined == undefined", "!0"); } + +#[test] +fn js_typeof() { + test("x = typeof 1", "x='number'"); + test("x = typeof 'foo'", "x='string'"); + test("x = typeof true", "x='boolean'"); + test("x = typeof false", "x='boolean'"); + test("x = typeof null", "x='object'"); + test("x = typeof undefined", "x='undefined'"); + test("x = typeof void 0", "x='undefined'"); + test("x = typeof []", "x='object'"); + test("x = typeof [1]", "x='object'"); + test("x = typeof [1,[]]", "x='object'"); + test("x = typeof {}", "x='object'"); + test("x = typeof function() {}", "x='function'"); + + test_same("x=typeof [1,[foo()]]"); + test_same("x=typeof {bathwater:baby()}"); +} diff --git a/tasks/minsize/minsize.snap b/tasks/minsize/minsize.snap index 7237d5171..392842ad7 100644 --- a/tasks/minsize/minsize.snap +++ b/tasks/minsize/minsize.snap @@ -11,7 +11,7 @@ Original -> Minified -> Gzip Brotli 555.77 kB -> 274.97 kB -> 91.38 kB 77.41 kB d3.js -977.19 kB -> 456.46 kB -> 123.82 kB 107.39 kB bundle.min.js +977.19 kB -> 456.45 kB -> 123.82 kB 107.40 kB bundle.min.js 1.25 MB -> 677.77 kB -> 166.79 kB 135.23 kB three.js