From fbfd852cf8415fb0f9f8e06bef4b6bb0613d3a00 Mon Sep 17 00:00:00 2001 From: Boshen <1430279+Boshen@users.noreply.github.com> Date: Thu, 8 Aug 2024 02:48:24 +0000 Subject: [PATCH] refactor(minifier): add `NodeUtil` trait for accessing symbols on ast nodes (#4734) --- .../src/ast_passes/fold_constants.rs | 151 +++-- crates/oxc_minifier/src/ast_passes/mod.rs | 13 + crates/oxc_minifier/src/ast_util.rs | 638 ------------------ crates/oxc_minifier/src/lib.rs | 2 +- .../src/node_util/check_for_state_change.rs | 144 ++++ .../src/node_util/is_literal_value.rs | 73 ++ .../src/node_util/may_have_side_effects.rs | 19 + crates/oxc_minifier/src/node_util/mod.rs | 379 +++++++++++ .../src/node_util/number_value.rs | 60 ++ .../tests/ast_passes/remove_dead_code.rs | 12 +- tasks/coverage/minifier_test262.snap | 6 +- 11 files changed, 790 insertions(+), 707 deletions(-) delete mode 100644 crates/oxc_minifier/src/ast_util.rs create mode 100644 crates/oxc_minifier/src/node_util/check_for_state_change.rs create mode 100644 crates/oxc_minifier/src/node_util/is_literal_value.rs create mode 100644 crates/oxc_minifier/src/node_util/may_have_side_effects.rs create mode 100644 crates/oxc_minifier/src/node_util/mod.rs create mode 100644 crates/oxc_minifier/src/node_util/number_value.rs diff --git a/crates/oxc_minifier/src/ast_passes/fold_constants.rs b/crates/oxc_minifier/src/ast_passes/fold_constants.rs index 04bb356b3cdaa..77473b41d77c9 100644 --- a/crates/oxc_minifier/src/ast_passes/fold_constants.rs +++ b/crates/oxc_minifier/src/ast_passes/fold_constants.rs @@ -15,12 +15,8 @@ use oxc_syntax::{ use oxc_traverse::{Traverse, TraverseCtx}; use crate::{ - ast_util::{ - get_boolean_value, get_number_value, get_side_free_bigint_value, - get_side_free_number_value, get_side_free_string_value, get_string_value, is_exact_int64, - MayHaveSideEffects, NumberValue, - }, keep_var::KeepVar, + node_util::{is_exact_int64, MayHaveSideEffects, NodeUtil, NumberValue}, tri::Tri, ty::Ty, CompressorPass, @@ -34,13 +30,13 @@ pub struct FoldConstants<'a> { impl<'a> CompressorPass<'a> for FoldConstants<'a> {} impl<'a> Traverse<'a> for FoldConstants<'a> { - fn exit_statement(&mut self, stmt: &mut Statement<'a>, _ctx: &mut TraverseCtx<'a>) { - self.fold_condition(stmt); + fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { + self.fold_condition(stmt, ctx); } - fn exit_expression(&mut self, expr: &mut Expression<'a>, _ctx: &mut TraverseCtx<'a>) { - self.fold_expression(expr); - self.fold_conditional_expression(expr); + fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { + self.fold_expression(expr, ctx); + self.fold_conditional_expression(expr, ctx); } } @@ -54,23 +50,27 @@ impl<'a> FoldConstants<'a> { self } - fn fold_expression_and_get_boolean_value(&mut self, expr: &mut Expression<'a>) -> Option { - self.fold_expression(expr); - get_boolean_value(expr) + fn fold_expression_and_get_boolean_value( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) -> Option { + self.fold_expression(expr, ctx); + ctx.get_boolean_value(expr) } - fn fold_if_statement(&mut self, stmt: &mut Statement<'a>) { + fn fold_if_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) { let Statement::IfStatement(if_stmt) = stmt else { return }; // Descend and remove `else` blocks first. if let Some(alternate) = &mut if_stmt.alternate { - self.fold_if_statement(alternate); + self.fold_if_statement(alternate, ctx); if matches!(alternate, Statement::EmptyStatement(_)) { if_stmt.alternate = None; } } - match self.fold_expression_and_get_boolean_value(&mut if_stmt.test) { + match self.fold_expression_and_get_boolean_value(&mut if_stmt.test, ctx) { Some(true) => { *stmt = self.ast.move_statement(&mut if_stmt.consequent); } @@ -90,11 +90,15 @@ impl<'a> FoldConstants<'a> { } } - fn fold_conditional_expression(&mut self, expr: &mut Expression<'a>) { + fn fold_conditional_expression( + &mut self, + expr: &mut Expression<'a>, + ctx: &mut TraverseCtx<'a>, + ) { let Expression::ConditionalExpression(conditional_expr) = expr else { return; }; - match self.fold_expression_and_get_boolean_value(&mut conditional_expr.test) { + match self.fold_expression_and_get_boolean_value(&mut conditional_expr.test, ctx) { Some(true) => { *expr = self.ast.move_expression(&mut conditional_expr.consequent); } @@ -105,7 +109,7 @@ impl<'a> FoldConstants<'a> { } } - pub fn fold_expression<'b>(&mut self, expr: &'b mut Expression<'a>) { + pub fn fold_expression<'b>(&mut self, expr: &'b mut Expression<'a>, ctx: &mut TraverseCtx<'a>) { let folded_expr = match expr { Expression::BinaryExpression(binary_expr) => match binary_expr.operator { BinaryOperator::Equality @@ -120,6 +124,7 @@ impl<'a> FoldConstants<'a> { binary_expr.operator, &binary_expr.left, &binary_expr.right, + ctx, ), BinaryOperator::ShiftLeft | BinaryOperator::ShiftRight @@ -128,6 +133,7 @@ impl<'a> FoldConstants<'a> { binary_expr.operator, &binary_expr.left, &binary_expr.right, + ctx, ), // NOTE: string concat folding breaks our current evaluation of Test262 tests. The // minifier is tested by comparing output of running the minifier once and twice, @@ -135,13 +141,16 @@ impl<'a> FoldConstants<'a> { // don't match (even though the produced code is valid). Additionally, We'll likely // want to add `evaluate` checks for all constant folding, not just additions, but // we're adding this here until a decision is made. - BinaryOperator::Addition if self.evaluate => { - self.try_fold_addition(binary_expr.span, &binary_expr.left, &binary_expr.right) - } + BinaryOperator::Addition if self.evaluate => self.try_fold_addition( + binary_expr.span, + &binary_expr.left, + &binary_expr.right, + ctx, + ), _ => None, }, Expression::LogicalExpression(logic_expr) => { - self.try_fold_logical_expression(logic_expr) + self.try_fold_logical_expression(logic_expr, ctx) } _ => None, }; @@ -155,6 +164,7 @@ impl<'a> FoldConstants<'a> { span: Span, left: &'b Expression<'a>, right: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Option> { // skip any potentially dangerous compressions if left.may_have_side_effects() || right.may_have_side_effects() { @@ -170,8 +180,8 @@ impl<'a> FoldConstants<'a> { (Ty::Str, _) | (_, Ty::Str) => { // no need to use get_side_effect_free_string_value b/c we checked for side effects // at the beginning - let left_string = get_string_value(left)?; - let right_string = get_string_value(right)?; + let left_string = ctx.get_string_value(left)?; + let right_string = ctx.get_string_value(right)?; // let value = left_string.to_owned(). let value = left_string + right_string; Some(self.ast.expression_string_literal(span, value)) @@ -181,8 +191,8 @@ impl<'a> FoldConstants<'a> { (Ty::Number, _) | (_, Ty::Number) // when added, booleans get treated as numbers where `true` is 1 and `false` is 0 | (Ty::Boolean, Ty::Boolean) => { - let left_number = get_number_value(left)?; - let right_number = get_number_value(right)?; + let left_number = ctx.get_number_value(left)?; + let right_number = ctx.get_number_value(right)?; let Ok(value) = TryInto::::try_into(left_number + right_number) else { return None }; // Float if value has a fractional part, otherwise Decimal let number_base = if is_exact_int64(value) { NumberBase::Decimal } else { NumberBase::Float }; @@ -199,8 +209,9 @@ impl<'a> FoldConstants<'a> { op: BinaryOperator, left: &'b Expression<'a>, right: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Option> { - let value = match self.evaluate_comparison(op, left, right) { + let value = match self.evaluate_comparison(op, left, right, ctx) { Tri::True => true, Tri::False => false, Tri::Unknown => return None, @@ -213,29 +224,34 @@ impl<'a> FoldConstants<'a> { op: BinaryOperator, left: &'b Expression<'a>, right: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Tri { if left.may_have_side_effects() || right.may_have_side_effects() { return Tri::Unknown; } match op { - BinaryOperator::Equality => self.try_abstract_equality_comparison(left, right), - BinaryOperator::Inequality => self.try_abstract_equality_comparison(left, right).not(), - BinaryOperator::StrictEquality => Self::try_strict_equality_comparison(left, right), + BinaryOperator::Equality => self.try_abstract_equality_comparison(left, right, ctx), + BinaryOperator::Inequality => { + self.try_abstract_equality_comparison(left, right, ctx).not() + } + BinaryOperator::StrictEquality => { + Self::try_strict_equality_comparison(left, right, ctx) + } BinaryOperator::StrictInequality => { - Self::try_strict_equality_comparison(left, right).not() + Self::try_strict_equality_comparison(left, right, ctx).not() } BinaryOperator::LessThan => { - Self::try_abstract_relational_comparison(left, right, false) + Self::try_abstract_relational_comparison(left, right, false, ctx) } BinaryOperator::GreaterThan => { - Self::try_abstract_relational_comparison(right, left, false) + Self::try_abstract_relational_comparison(right, left, false, ctx) } BinaryOperator::LessEqualThan => { - Self::try_abstract_relational_comparison(right, left, true).not() + Self::try_abstract_relational_comparison(right, left, true, ctx).not() } BinaryOperator::GreaterEqualThan => { - Self::try_abstract_relational_comparison(left, right, true).not() + Self::try_abstract_relational_comparison(left, right, true, ctx).not() } _ => Tri::Unknown, } @@ -246,19 +262,20 @@ impl<'a> FoldConstants<'a> { &mut self, left_expr: &'b Expression<'a>, right_expr: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Tri { let left = Ty::from(left_expr); let right = Ty::from(right_expr); if left != Ty::Undetermined && right != Ty::Undetermined { if left == right { - return Self::try_strict_equality_comparison(left_expr, right_expr); + return Self::try_strict_equality_comparison(left_expr, right_expr, ctx); } if matches!((left, right), (Ty::Null, Ty::Void) | (Ty::Void, Ty::Null)) { 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); + let right_number = ctx.get_side_free_number_value(right_expr); if let Some(NumberValue::Number(num)) = right_number { let number_literal_expr = self.ast.expression_numeric_literal( @@ -268,14 +285,18 @@ impl<'a> FoldConstants<'a> { if num.fract() == 0.0 { NumberBase::Decimal } else { NumberBase::Float }, ); - return self.try_abstract_equality_comparison(left_expr, &number_literal_expr); + return self.try_abstract_equality_comparison( + left_expr, + &number_literal_expr, + ctx, + ); } 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); + let left_number = ctx.get_side_free_number_value(left_expr); if let Some(NumberValue::Number(num)) = left_number { let number_literal_expr = self.ast.expression_numeric_literal( @@ -285,15 +306,19 @@ impl<'a> FoldConstants<'a> { if num.fract() == 0.0 { NumberBase::Decimal } else { NumberBase::Float }, ); - return self.try_abstract_equality_comparison(&number_literal_expr, right_expr); + return self.try_abstract_equality_comparison( + &number_literal_expr, + right_expr, + ctx, + ); } 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); + let left_bigint = ctx.get_side_free_bigint_value(left_expr); + let right_bigint = ctx.get_side_free_bigint_value(right_expr); if let (Some(l_big), Some(r_big)) = (left_bigint, right_bigint) { return Tri::for_boolean(l_big.eq(&r_big)); @@ -318,14 +343,15 @@ impl<'a> FoldConstants<'a> { left_expr: &'b Expression<'a>, right_expr: &'b Expression<'a>, will_negative: bool, + ctx: &mut TraverseCtx<'a>, ) -> Tri { let left = Ty::from(left_expr); let right = Ty::from(right_expr); // First, check for a string comparison. if left == Ty::Str && right == Ty::Str { - let left_string = get_side_free_string_value(left_expr); - let right_string = get_side_free_string_value(right_expr); + let left_string = ctx.get_side_free_string_value(left_expr); + let right_string = ctx.get_side_free_string_value(right_expr); if let (Some(left_string), Some(right_string)) = (left_string, right_string) { // In JS, browsers parse \v differently. So do not compare strings if one contains \v. if left_string.contains('\u{000B}') || right_string.contains('\u{000B}') { @@ -352,11 +378,11 @@ impl<'a> FoldConstants<'a> { } } - let left_bigint = get_side_free_bigint_value(left_expr); - let right_bigint = get_side_free_bigint_value(right_expr); + let left_bigint = ctx.get_side_free_bigint_value(left_expr); + let right_bigint = ctx.get_side_free_bigint_value(right_expr); - let left_num = get_side_free_number_value(left_expr); - let right_num = get_side_free_number_value(right_expr); + let left_num = ctx.get_side_free_number_value(left_expr); + let right_num = ctx.get_side_free_number_value(right_expr); match (left_bigint, right_bigint, left_num, right_num) { // Next, try to evaluate based on the value of the node. Try comparing as BigInts first. @@ -426,6 +452,7 @@ impl<'a> FoldConstants<'a> { fn try_strict_equality_comparison<'b>( left_expr: &'b Expression<'a>, right_expr: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Tri { let left = Ty::from(left_expr); let right = Ty::from(right_expr); @@ -436,8 +463,8 @@ impl<'a> FoldConstants<'a> { } 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); + let left_number = ctx.get_side_free_number_value(left_expr); + let right_number = ctx.get_side_free_number_value(right_expr); if let (Some(l_num), Some(r_num)) = (left_number, right_number) { if l_num.is_nan() || r_num.is_nan() { @@ -450,8 +477,8 @@ impl<'a> FoldConstants<'a> { Tri::Unknown } Ty::Str => { - let left_string = get_side_free_string_value(left_expr); - let right_string = get_side_free_string_value(right_expr); + let left_string = ctx.get_side_free_string_value(left_expr); + let right_string = ctx.get_side_free_string_value(right_expr); if let (Some(left_string), Some(right_string)) = (left_string, right_string) { // In JS, browsers parse \v differently. So do not compare strings if one contains \v. if left_string.contains('\u{000B}') || right_string.contains('\u{000B}') { @@ -502,9 +529,10 @@ impl<'a> FoldConstants<'a> { op: BinaryOperator, left: &'b Expression<'a>, right: &'b Expression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Option> { - let left_num = get_side_free_number_value(left); - let right_num = get_side_free_number_value(right); + let left_num = ctx.get_side_free_number_value(left); + let right_num = ctx.get_side_free_number_value(right); if let (Some(NumberValue::Number(left_val)), Some(NumberValue::Number(right_val))) = (left_num, right_num) @@ -552,12 +580,13 @@ impl<'a> FoldConstants<'a> { pub fn try_fold_logical_expression( &mut self, logical_expr: &mut LogicalExpression<'a>, + ctx: &mut TraverseCtx<'a>, ) -> Option> { let op = logical_expr.operator; if !matches!(op, LogicalOperator::And | LogicalOperator::Or) { return None; } - if let Some(boolean_value) = get_boolean_value(&logical_expr.left) { + if let Some(boolean_value) = ctx.get_boolean_value(&logical_expr.left) { // (TRUE || x) => TRUE (also, (3 || x) => 3) // (FALSE && x) => FALSE if (boolean_value && op == LogicalOperator::Or) @@ -581,7 +610,7 @@ impl<'a> FoldConstants<'a> { return Some(sequence_expr); } else if let Expression::LogicalExpression(left_child) = &mut logical_expr.left { if left_child.operator == logical_expr.operator { - let left_child_right_boolean = get_boolean_value(&left_child.right); + let left_child_right_boolean = ctx.get_boolean_value(&left_child.right); let left_child_op = left_child.operator; if let Some(right_boolean) = left_child_right_boolean { if !left_child.right.may_have_side_effects() { @@ -607,7 +636,11 @@ impl<'a> FoldConstants<'a> { None } - pub(crate) fn fold_condition<'b>(&mut self, stmt: &'b mut Statement<'a>) { + pub(crate) fn fold_condition<'b>( + &mut self, + stmt: &'b mut Statement<'a>, + ctx: &mut TraverseCtx<'a>, + ) { match stmt { Statement::WhileStatement(while_stmt) => { let minimized_expr = self.fold_expression_in_condition(&mut while_stmt.test); @@ -628,7 +661,7 @@ impl<'a> FoldConstants<'a> { } } Statement::IfStatement(_) => { - self.fold_if_statement(stmt); + self.fold_if_statement(stmt, ctx); } _ => {} }; diff --git a/crates/oxc_minifier/src/ast_passes/mod.rs b/crates/oxc_minifier/src/ast_passes/mod.rs index 20d29dde1e6dc..be21f5a4dce66 100644 --- a/crates/oxc_minifier/src/ast_passes/mod.rs +++ b/crates/oxc_minifier/src/ast_passes/mod.rs @@ -11,8 +11,21 @@ pub use remove_syntax::RemoveSyntax; pub use substitute_alternate_syntax::SubstituteAlternateSyntax; use oxc_ast::ast::Program; +use oxc_semantic::{ScopeTree, SymbolTable}; use oxc_traverse::{walk_program, Traverse, TraverseCtx}; +use crate::node_util::NodeUtil; + +impl<'a> NodeUtil for TraverseCtx<'a> { + fn symbols(&self) -> &SymbolTable { + self.scoping.symbols() + } + + fn scopes(&self) -> &ScopeTree { + self.scoping.scopes() + } +} + pub trait CompressorPass<'a> { fn build(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) where diff --git a/crates/oxc_minifier/src/ast_util.rs b/crates/oxc_minifier/src/ast_util.rs deleted file mode 100644 index 2e1eab349e3c6..0000000000000 --- a/crates/oxc_minifier/src/ast_util.rs +++ /dev/null @@ -1,638 +0,0 @@ -use std::borrow::Cow; - -use num_bigint::BigInt; -use num_traits::{One, Zero}; -use oxc_ast::ast::{ - match_expression, ArrayExpressionElement, BinaryExpression, Expression, NumericLiteral, - ObjectProperty, ObjectPropertyKind, PropertyKey, SpreadElement, UnaryExpression, -}; -use oxc_semantic::ReferenceFlag; -use oxc_syntax::operator::{AssignmentOperator, LogicalOperator, UnaryOperator}; - -/// 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::ArrowFunctionExpression(_) => 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), - match_expression!(Self) => self.to_expression().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::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, - match_expression!(Self) => self.to_expression().is_literal_value(include_functions), - } - } -} - -/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L94) -/// Returns true if the node which may have side effects when executed. -/// This version default to the "safe" assumptions when the compiler object -/// is not provided (RegExp have side-effects, etc). -pub trait MayHaveSideEffects<'a, 'b> -where - Self: CheckForStateChange<'a, 'b>, -{ - fn may_have_side_effects(&self) -> bool { - self.check_for_state_change(false) - } -} - -/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L241) -/// Returns true if some node in n's subtree changes application state. If -/// `check_for_new_objects` is true, we assume that newly created mutable objects (like object -/// literals) change state. Otherwise, we assume that they have no side effects. -pub trait CheckForStateChange<'a, 'b> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool; -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for Expression<'a> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { - match self { - Self::NumericLiteral(_) - | Self::BooleanLiteral(_) - | Self::StringLiteral(_) - | Self::BigIntLiteral(_) - | Self::NullLiteral(_) - | Self::RegExpLiteral(_) - | Self::MetaProperty(_) - | Self::ThisExpression(_) - | Self::ClassExpression(_) - | Self::FunctionExpression(_) => false, - Self::TemplateLiteral(template) => template - .expressions - .iter() - .any(|expr| expr.check_for_state_change(check_for_new_objects)), - Self::Identifier(ident) => ident.reference_flag == ReferenceFlag::Write, - Self::UnaryExpression(unary_expr) => { - unary_expr.check_for_state_change(check_for_new_objects) - } - Self::ParenthesizedExpression(p) => { - p.expression.check_for_state_change(check_for_new_objects) - } - Self::ConditionalExpression(p) => { - p.test.check_for_state_change(check_for_new_objects) - || p.consequent.check_for_state_change(check_for_new_objects) - || p.alternate.check_for_state_change(check_for_new_objects) - } - Self::SequenceExpression(s) => { - s.expressions.iter().any(|expr| 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; - } - - object_expr - .properties - .iter() - .any(|property| property.check_for_state_change(check_for_new_objects)) - } - Self::ArrayExpression(array_expr) => { - if check_for_new_objects { - return true; - } - array_expr - .elements - .iter() - .any(|element| element.check_for_state_change(check_for_new_objects)) - } - _ => true, - } - } -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for UnaryExpression<'a> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { - if is_simple_unary_operator(self.operator) { - return self.argument.check_for_state_change(check_for_new_objects); - } - true - } -} - -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 { - Self::SpreadElement(element) => element.check_for_state_change(check_for_new_objects), - match_expression!(Self) => { - self.to_expression().check_for_state_change(check_for_new_objects) - } - Self::Elision(_) => false, - } - } -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectPropertyKind<'a> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { - match self { - Self::ObjectProperty(method) => method.check_for_state_change(check_for_new_objects), - Self::SpreadProperty(spread_element) => { - spread_element.check_for_state_change(check_for_new_objects) - } - } - } -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for SpreadElement<'a> { - fn check_for_state_change(&self, _check_for_new_objects: bool) -> bool { - // Object-rest and object-spread may trigger a getter. - // TODO: Closure Compiler assumes that getters may side-free when set `assumeGettersArePure`. - // https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AstAnalyzer.java#L282 - true - } -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectProperty<'a> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { - self.key.check_for_state_change(check_for_new_objects) - || self.value.check_for_state_change(check_for_new_objects) - } -} - -impl<'a, 'b> CheckForStateChange<'a, 'b> for PropertyKey<'a> { - fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { - match self { - Self::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, - match_expression!(Self) => { - self.to_expression().check_for_state_change(check_for_new_objects) - } - } - } -} - -impl<'a, 'b> MayHaveSideEffects<'a, 'b> for Expression<'a> {} -impl<'a, 'b> MayHaveSideEffects<'a, 'b> for UnaryExpression<'a> {} - -/// A "simple" operator is one whose children are expressions, has no direct side-effects. -fn is_simple_unary_operator(operator: UnaryOperator) -> bool { - operator != UnaryOperator::Delete -} - -#[derive(PartialEq)] -pub enum NumberValue { - Number(f64), - PositiveInfinity, - NegativeInfinity, - NaN, -} - -impl NumberValue { - #[must_use] - pub fn not(&self) -> Self { - match self { - Self::Number(num) => Self::Number(-num), - Self::PositiveInfinity => Self::NegativeInfinity, - Self::NegativeInfinity => Self::PositiveInfinity, - Self::NaN => Self::NaN, - } - } - - pub fn is_nan(&self) -> bool { - matches!(self, Self::NaN) - } -} - -impl std::ops::Add for NumberValue { - type Output = Self; - - fn add(self, other: Self) -> Self { - match self { - Self::Number(num) => match other { - Self::Number(other_num) => Self::Number(num + other_num), - Self::PositiveInfinity => Self::PositiveInfinity, - Self::NegativeInfinity => Self::NegativeInfinity, - Self::NaN => Self::NaN, - }, - Self::NaN => Self::NaN, - Self::PositiveInfinity => match other { - Self::NaN | Self::NegativeInfinity => Self::NaN, - _ => Self::PositiveInfinity, - }, - Self::NegativeInfinity => match other { - Self::NaN | Self::PositiveInfinity => Self::NaN, - _ => Self::NegativeInfinity, - }, - } - } -} - -impl TryFrom for f64 { - type Error = (); - - fn try_from(value: NumberValue) -> Result { - match value { - NumberValue::Number(num) => Ok(num), - NumberValue::PositiveInfinity => Ok(Self::INFINITY), - NumberValue::NegativeInfinity => Ok(Self::NEG_INFINITY), - NumberValue::NaN => Err(()), - } - } -} - -pub fn is_exact_int64(num: f64) -> bool { - num.fract() == 0.0 -} - -/// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/NodeUtil.java#L540) -pub fn get_string_bigint_value(raw_string: &str) -> Option { - if raw_string.contains('\u{000b}') { - // vertical tab is not always whitespace - return None; - } - - let s = raw_string.trim(); - - if s.is_empty() { - return Some(BigInt::zero()); - } - - if s.len() > 2 && s.starts_with('0') { - let radix: u32 = match s.chars().nth(1) { - Some('x' | 'X') => 16, - Some('o' | 'O') => 8, - Some('b' | 'B') => 2, - _ => 0, - }; - - if radix == 0 { - return None; - } - - return BigInt::parse_bytes(s[2..].as_bytes(), radix); - } - - return BigInt::parse_bytes(s.as_bytes(), 10); -} - -/// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L348) -/// Gets the value of a node as a Number, or None if it cannot be converted. -/// This method does not consider whether `expr` may have side effects. -pub fn get_number_value(expr: &Expression) -> Option { - match expr { - Expression::NumericLiteral(number_literal) => { - Some(NumberValue::Number(number_literal.value)) - } - Expression::UnaryExpression(unary_expr) => match unary_expr.operator { - UnaryOperator::UnaryPlus => get_number_value(&unary_expr.argument), - UnaryOperator::UnaryNegation => get_number_value(&unary_expr.argument).map(|v| v.not()), - UnaryOperator::BitwiseNot => get_number_value(&unary_expr.argument).map(|value| { - match value { - NumberValue::Number(num) => { - NumberValue::Number(f64::from(!NumericLiteral::ecmascript_to_int32(num))) - } - // ~Infinity -> -1 - // ~-Infinity -> -1 - // ~NaN -> -1 - _ => NumberValue::Number(-1_f64), - } - }), - UnaryOperator::LogicalNot => get_boolean_value(expr) - .map(|boolean| if boolean { 1_f64 } else { 0_f64 }) - .map(NumberValue::Number), - UnaryOperator::Void => Some(NumberValue::NaN), - _ => None, - }, - Expression::BooleanLiteral(bool_literal) => { - if bool_literal.value { - Some(NumberValue::Number(1.0)) - } else { - Some(NumberValue::Number(0.0)) - } - } - Expression::NullLiteral(_) => Some(NumberValue::Number(0.0)), - Expression::Identifier(ident) => match ident.name.as_str() { - "Infinity" => Some(NumberValue::PositiveInfinity), - "NaN" | "undefined" => Some(NumberValue::NaN), - _ => None, - }, - // TODO: will be implemented in next PR, just for test pass now. - Expression::StringLiteral(string_literal) => string_literal - .value - .parse::() - .map_or(Some(NumberValue::NaN), |num| Some(NumberValue::Number(num))), - _ => None, - } -} - -#[allow(clippy::cast_possible_truncation)] -pub fn get_bigint_value(expr: &Expression) -> Option { - match expr { - Expression::NumericLiteral(number_literal) => { - let value = number_literal.value; - if value.abs() < 2_f64.powi(53) && is_exact_int64(value) { - Some(BigInt::from(value as i64)) - } else { - None - } - } - Expression::BigIntLiteral(_bigint_literal) => { - // TODO: evaluate the bigint value - None - } - Expression::BooleanLiteral(bool_literal) => { - if bool_literal.value { - Some(BigInt::one()) - } else { - Some(BigInt::zero()) - } - } - Expression::UnaryExpression(unary_expr) => match unary_expr.operator { - UnaryOperator::LogicalNot => { - get_boolean_value(expr) - .map(|boolean| if boolean { BigInt::one() } else { BigInt::zero() }) - } - UnaryOperator::UnaryNegation => { - get_bigint_value(&unary_expr.argument).map(std::ops::Neg::neg) - } - UnaryOperator::BitwiseNot => { - get_bigint_value(&unary_expr.argument).map(std::ops::Not::not) - } - UnaryOperator::UnaryPlus => get_bigint_value(&unary_expr.argument), - _ => None, - }, - Expression::StringLiteral(string_literal) => get_string_bigint_value(&string_literal.value), - Expression::TemplateLiteral(_) => { - get_string_value(expr).and_then(|value| get_string_bigint_value(&value)) - } - _ => None, - } -} - -/// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L104-L114) -/// Returns the number value of the node if it has one and it cannot have side effects. -pub fn get_side_free_number_value(expr: &Expression) -> Option { - let value = get_number_value(expr); - // Calculating the number value, if any, is likely to be faster than calculating side effects, - // and there are only a very few cases where we can compute a number value, but there could - // also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior - // of `doSomething()` - if value.is_some() && expr.may_have_side_effects() { - None - } else { - value - } -} - -/// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L121) -pub fn get_side_free_bigint_value(expr: &Expression) -> Option { - let value = get_bigint_value(expr); - // Calculating the bigint value, if any, is likely to be faster than calculating side effects, - // and there are only a very few cases where we can compute a bigint value, but there could - // also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior - // of `doSomething()` - if value.is_some() && expr.may_have_side_effects() { - None - } else { - value - } -} - -/// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L109) -/// Gets the boolean value of a node that represents an expression, or `None` if no -/// such value can be determined by static analysis. -/// This method does not consider whether the node may have side-effects. -pub fn get_boolean_value(expr: &Expression) -> Option { - match expr { - Expression::RegExpLiteral(_) - | Expression::ArrayExpression(_) - | Expression::ArrowFunctionExpression(_) - | Expression::ClassExpression(_) - | Expression::FunctionExpression(_) - | Expression::NewExpression(_) - | Expression::ObjectExpression(_) => Some(true), - Expression::NullLiteral(_) => Some(false), - Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value), - Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0), - Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()), - Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()), - Expression::TemplateLiteral(template_literal) => { - // only for `` - template_literal - .quasis - .first() - .filter(|quasi| quasi.tail) - .and_then(|quasi| quasi.value.cooked.as_ref()) - .map(|cooked| !cooked.is_empty()) - } - Expression::Identifier(ident) => match ident.name.as_str() { - "NaN" => Some(false), - "Infinity" => Some(true), - // "undefined" if ident.reference_id.get().is_none() => Some(false), - _ => None, - }, - Expression::AssignmentExpression(assign_expr) => { - match assign_expr.operator { - AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None, - // For ASSIGN, the value is the value of the RHS. - _ => get_boolean_value(&assign_expr.right), - } - } - Expression::LogicalExpression(logical_expr) => { - match logical_expr.operator { - // true && true -> true - // true && false -> false - // a && true -> None - LogicalOperator::And => { - let left = get_boolean_value(&logical_expr.left); - let right = get_boolean_value(&logical_expr.right); - - match (left, right) { - (Some(true), Some(true)) => Some(true), - (Some(false), _) | (_, Some(false)) => Some(false), - (None, _) | (_, None) => None, - } - } - // true || false -> true - // false || false -> false - // a || b -> None - LogicalOperator::Or => { - let left = get_boolean_value(&logical_expr.left); - let right = get_boolean_value(&logical_expr.right); - - match (left, right) { - (Some(true), _) | (_, Some(true)) => Some(true), - (Some(false), Some(false)) => Some(false), - (None, _) | (_, None) => None, - } - } - LogicalOperator::Coalesce => None, - } - } - Expression::SequenceExpression(sequence_expr) => { - // For sequence expression, the value is the value of the RHS. - sequence_expr.expressions.last().and_then(get_boolean_value) - } - Expression::UnaryExpression(unary_expr) => { - if unary_expr.operator == UnaryOperator::Void { - Some(false) - } else if matches!( - unary_expr.operator, - UnaryOperator::BitwiseNot | UnaryOperator::UnaryPlus | UnaryOperator::UnaryNegation - ) { - // ~0 -> true - // +1 -> true - // +0 -> false - // -0 -> false - get_number_value(expr).map(|value| value != NumberValue::Number(0_f64)) - } else if unary_expr.operator == UnaryOperator::LogicalNot { - // !true -> false - get_boolean_value(&unary_expr.argument).map(|boolean| !boolean) - } else { - None - } - } - _ => None, - } -} - -/// Port from [closure-compiler](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L234) -/// Gets the value of a node as a String, or `None` if it cannot be converted. When it returns a -/// String, this method effectively emulates the `String()` JavaScript cast function. -/// This method does not consider whether `expr` may have side effects. -pub fn get_string_value<'a>(expr: &'a Expression) -> Option> { - match expr { - Expression::StringLiteral(string_literal) => { - Some(Cow::Borrowed(string_literal.value.as_str())) - } - Expression::TemplateLiteral(template_literal) => { - // TODO: I don't know how to iterate children of TemplateLiteral in order,so only checkout string like `hi`. - // Closure-compiler do more: [case TEMPLATELIT](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L241-L256). - template_literal - .quasis - .first() - .filter(|quasi| quasi.tail) - .and_then(|quasi| quasi.value.cooked.as_ref()) - .map(|cooked| Cow::Borrowed(cooked.as_str())) - } - Expression::Identifier(ident) => { - let name = ident.name.as_str(); - if matches!(name, "undefined" | "Infinity" | "NaN") { - Some(Cow::Borrowed(name)) - } else { - None - } - } - Expression::NumericLiteral(number_literal) => { - Some(Cow::Owned(number_literal.value.to_string())) - } - Expression::BigIntLiteral(big_int_literal) => { - Some(Cow::Owned(big_int_literal.raw.to_string())) - } - Expression::NullLiteral(_) => Some(Cow::Borrowed("null")), - Expression::BooleanLiteral(bool_literal) => { - if bool_literal.value { - Some(Cow::Borrowed("true")) - } else { - Some(Cow::Borrowed("false")) - } - } - Expression::UnaryExpression(unary_expr) => { - match unary_expr.operator { - UnaryOperator::Void => Some(Cow::Borrowed("undefined")), - UnaryOperator::LogicalNot => { - get_boolean_value(&unary_expr.argument).map(|boolean| { - // need reversed. - if boolean { - Cow::Borrowed("false") - } else { - Cow::Borrowed("true") - } - }) - } - _ => None, - } - } - Expression::ArrayExpression(_) => { - // TODO: https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L302-L303 - None - } - Expression::ObjectExpression(_) => Some(Cow::Borrowed("[object Object]")), - _ => None, - } -} - -/// Port from [closure-compiler](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L139-L149) -/// Gets the value of a node as a String, or `None` if it cannot be converted. -/// This method effectively emulates the `String()` JavaScript cast function when -/// possible and the node has no side effects. Otherwise, it returns `None`. -pub fn get_side_free_string_value<'a>(expr: &'a Expression) -> Option> { - let value = get_string_value(expr); - // Calculating the string value, if any, is likely to be faster than calculating side effects, - // and there are only a very few cases where we can compute a string value, but there could - // also be side effects. e.g. `void doSomething()` has value 'undefined', regardless of the - // behavior of `doSomething()` - if value.is_some() && !expr.may_have_side_effects() { - return value; - } - None -} diff --git a/crates/oxc_minifier/src/lib.rs b/crates/oxc_minifier/src/lib.rs index 148df0c46533a..94cdbff5cc2eb 100644 --- a/crates/oxc_minifier/src/lib.rs +++ b/crates/oxc_minifier/src/lib.rs @@ -3,9 +3,9 @@ //! ECMAScript Minifier mod ast_passes; -mod ast_util; mod compressor; mod keep_var; +mod node_util; mod options; mod plugins; mod tri; diff --git a/crates/oxc_minifier/src/node_util/check_for_state_change.rs b/crates/oxc_minifier/src/node_util/check_for_state_change.rs new file mode 100644 index 0000000000000..6281449f2d7d0 --- /dev/null +++ b/crates/oxc_minifier/src/node_util/check_for_state_change.rs @@ -0,0 +1,144 @@ +use oxc_ast::ast::*; + +use oxc_semantic::ReferenceFlag; +use oxc_syntax::operator::UnaryOperator; + +/// A "simple" operator is one whose children are expressions, has no direct side-effects. +fn is_simple_unary_operator(operator: UnaryOperator) -> bool { + operator != UnaryOperator::Delete +} + +/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L241) +/// Returns true if some node in n's subtree changes application state. If +/// `check_for_new_objects` is true, we assume that newly created mutable objects (like object +/// literals) change state. Otherwise, we assume that they have no side effects. +pub trait CheckForStateChange<'a, 'b> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool; +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for Expression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::NumericLiteral(_) + | Self::BooleanLiteral(_) + | Self::StringLiteral(_) + | Self::BigIntLiteral(_) + | Self::NullLiteral(_) + | Self::RegExpLiteral(_) + | Self::MetaProperty(_) + | Self::ThisExpression(_) + | Self::ClassExpression(_) + | Self::FunctionExpression(_) => false, + Self::TemplateLiteral(template) => template + .expressions + .iter() + .any(|expr| expr.check_for_state_change(check_for_new_objects)), + Self::Identifier(ident) => ident.reference_flag == ReferenceFlag::Write, + Self::UnaryExpression(unary_expr) => { + unary_expr.check_for_state_change(check_for_new_objects) + } + Self::ParenthesizedExpression(p) => { + p.expression.check_for_state_change(check_for_new_objects) + } + Self::ConditionalExpression(p) => { + p.test.check_for_state_change(check_for_new_objects) + || p.consequent.check_for_state_change(check_for_new_objects) + || p.alternate.check_for_state_change(check_for_new_objects) + } + Self::SequenceExpression(s) => { + s.expressions.iter().any(|expr| 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; + } + + object_expr + .properties + .iter() + .any(|property| property.check_for_state_change(check_for_new_objects)) + } + Self::ArrayExpression(array_expr) => { + if check_for_new_objects { + return true; + } + array_expr + .elements + .iter() + .any(|element| element.check_for_state_change(check_for_new_objects)) + } + _ => true, + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for UnaryExpression<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + if is_simple_unary_operator(self.operator) { + return self.argument.check_for_state_change(check_for_new_objects); + } + true + } +} + +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 { + Self::SpreadElement(element) => element.check_for_state_change(check_for_new_objects), + match_expression!(Self) => { + self.to_expression().check_for_state_change(check_for_new_objects) + } + Self::Elision(_) => false, + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectPropertyKind<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::ObjectProperty(method) => method.check_for_state_change(check_for_new_objects), + Self::SpreadProperty(spread_element) => { + spread_element.check_for_state_change(check_for_new_objects) + } + } + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for SpreadElement<'a> { + fn check_for_state_change(&self, _check_for_new_objects: bool) -> bool { + // Object-rest and object-spread may trigger a getter. + // TODO: Closure Compiler assumes that getters may side-free when set `assumeGettersArePure`. + // https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AstAnalyzer.java#L282 + true + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for ObjectProperty<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + self.key.check_for_state_change(check_for_new_objects) + || self.value.check_for_state_change(check_for_new_objects) + } +} + +impl<'a, 'b> CheckForStateChange<'a, 'b> for PropertyKey<'a> { + fn check_for_state_change(&self, check_for_new_objects: bool) -> bool { + match self { + Self::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, + match_expression!(Self) => { + self.to_expression().check_for_state_change(check_for_new_objects) + } + } + } +} diff --git a/crates/oxc_minifier/src/node_util/is_literal_value.rs b/crates/oxc_minifier/src/node_util/is_literal_value.rs new file mode 100644 index 0000000000000..996a02ed4f31c --- /dev/null +++ b/crates/oxc_minifier/src/node_util/is_literal_value.rs @@ -0,0 +1,73 @@ +use oxc_ast::ast::*; + +/// 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::ArrowFunctionExpression(_) => 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), + match_expression!(Self) => self.to_expression().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::StaticIdentifier(_) | Self::PrivateIdentifier(_) => false, + match_expression!(Self) => self.to_expression().is_literal_value(include_functions), + } + } +} diff --git a/crates/oxc_minifier/src/node_util/may_have_side_effects.rs b/crates/oxc_minifier/src/node_util/may_have_side_effects.rs new file mode 100644 index 0000000000000..e6ec638d381d7 --- /dev/null +++ b/crates/oxc_minifier/src/node_util/may_have_side_effects.rs @@ -0,0 +1,19 @@ +use oxc_ast::ast::*; + +use super::check_for_state_change::CheckForStateChange; + +/// port from [closure-compiler](https://github.com/google/closure-compiler/blob/f3ce5ed8b630428e311fe9aa2e20d36560d975e2/src/com/google/javascript/jscomp/AstAnalyzer.java#L94) +/// Returns true if the node which may have side effects when executed. +/// This version default to the "safe" assumptions when the compiler object +/// is not provided (RegExp have side-effects, etc). +pub trait MayHaveSideEffects<'a, 'b> +where + Self: CheckForStateChange<'a, 'b>, +{ + fn may_have_side_effects(&self) -> bool { + self.check_for_state_change(false) + } +} + +impl<'a, 'b> MayHaveSideEffects<'a, 'b> for Expression<'a> {} +impl<'a, 'b> MayHaveSideEffects<'a, 'b> for UnaryExpression<'a> {} diff --git a/crates/oxc_minifier/src/node_util/mod.rs b/crates/oxc_minifier/src/node_util/mod.rs new file mode 100644 index 0000000000000..67c0e530ee8eb --- /dev/null +++ b/crates/oxc_minifier/src/node_util/mod.rs @@ -0,0 +1,379 @@ +mod check_for_state_change; +mod is_literal_value; +mod may_have_side_effects; +mod number_value; + +use std::borrow::Cow; + +use num_bigint::BigInt; +use num_traits::{One, Zero}; + +use oxc_ast::ast::*; +use oxc_semantic::{ScopeTree, SymbolTable}; +use oxc_syntax::operator::{AssignmentOperator, LogicalOperator, UnaryOperator}; + +pub use self::{may_have_side_effects::MayHaveSideEffects, number_value::NumberValue}; + +pub fn is_exact_int64(num: f64) -> bool { + num.fract() == 0.0 +} + +pub trait NodeUtil { + fn symbols(&self) -> &SymbolTable; + + #[allow(unused)] + fn scopes(&self) -> &ScopeTree; + + /// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L104-L114) + /// Returns the number value of the node if it has one and it cannot have side effects. + fn get_side_free_number_value(&self, expr: &Expression) -> Option { + let value = self.get_number_value(expr); + // Calculating the number value, if any, is likely to be faster than calculating side effects, + // and there are only a very few cases where we can compute a number value, but there could + // also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior + // of `doSomething()` + if value.is_some() && expr.may_have_side_effects() { + None + } else { + value + } + } + + /// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L121) + fn get_side_free_bigint_value(&self, expr: &Expression) -> Option { + let value = self.get_bigint_value(expr); + // Calculating the bigint value, if any, is likely to be faster than calculating side effects, + // and there are only a very few cases where we can compute a bigint value, but there could + // also be side effects. e.g. `void doSomething()` has value NaN, regardless of the behavior + // of `doSomething()` + if value.is_some() && expr.may_have_side_effects() { + None + } else { + value + } + } + + /// Port from [closure-compiler](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/AbstractPeepholeOptimization.java#L139-L149) + /// Gets the value of a node as a String, or `None` if it cannot be converted. + /// This method effectively emulates the `String()` JavaScript cast function when + /// possible and the node has no side effects. Otherwise, it returns `None`. + fn get_side_free_string_value<'a>(&self, expr: &'a Expression) -> Option> { + let value = self.get_string_value(expr); + // Calculating the string value, if any, is likely to be faster than calculating side effects, + // and there are only a very few cases where we can compute a string value, but there could + // also be side effects. e.g. `void doSomething()` has value 'undefined', regardless of the + // behavior of `doSomething()` + if value.is_some() && !expr.may_have_side_effects() { + return value; + } + None + } + + /// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L109) + /// Gets the boolean value of a node that represents an expression, or `None` if no + /// such value can be determined by static analysis. + /// This method does not consider whether the node may have side-effects. + fn get_boolean_value(&self, expr: &Expression) -> Option { + match expr { + Expression::RegExpLiteral(_) + | Expression::ArrayExpression(_) + | Expression::ArrowFunctionExpression(_) + | Expression::ClassExpression(_) + | Expression::FunctionExpression(_) + | Expression::NewExpression(_) + | Expression::ObjectExpression(_) => Some(true), + Expression::NullLiteral(_) => Some(false), + Expression::BooleanLiteral(boolean_literal) => Some(boolean_literal.value), + Expression::NumericLiteral(number_literal) => Some(number_literal.value != 0.0), + Expression::BigIntLiteral(big_int_literal) => Some(!big_int_literal.is_zero()), + Expression::StringLiteral(string_literal) => Some(!string_literal.value.is_empty()), + Expression::TemplateLiteral(template_literal) => { + // only for `` + template_literal + .quasis + .first() + .filter(|quasi| quasi.tail) + .and_then(|quasi| quasi.value.cooked.as_ref()) + .map(|cooked| !cooked.is_empty()) + } + Expression::Identifier(ident) => match ident.name.as_str() { + "NaN" => Some(false), + "Infinity" => Some(true), + "undefined" + if ident + .reference_id + .get() + .is_some_and(|id| self.symbols().is_global_reference(id)) => + { + Some(false) + } + _ => None, + }, + Expression::AssignmentExpression(assign_expr) => { + match assign_expr.operator { + AssignmentOperator::LogicalAnd | AssignmentOperator::LogicalOr => None, + // For ASSIGN, the value is the value of the RHS. + _ => self.get_boolean_value(&assign_expr.right), + } + } + Expression::LogicalExpression(logical_expr) => { + match logical_expr.operator { + // true && true -> true + // true && false -> false + // a && true -> None + LogicalOperator::And => { + let left = self.get_boolean_value(&logical_expr.left); + let right = self.get_boolean_value(&logical_expr.right); + + match (left, right) { + (Some(true), Some(true)) => Some(true), + (Some(false), _) | (_, Some(false)) => Some(false), + (None, _) | (_, None) => None, + } + } + // true || false -> true + // false || false -> false + // a || b -> None + LogicalOperator::Or => { + let left = self.get_boolean_value(&logical_expr.left); + let right = self.get_boolean_value(&logical_expr.right); + + match (left, right) { + (Some(true), _) | (_, Some(true)) => Some(true), + (Some(false), Some(false)) => Some(false), + (None, _) | (_, None) => None, + } + } + LogicalOperator::Coalesce => None, + } + } + Expression::SequenceExpression(sequence_expr) => { + // For sequence expression, the value is the value of the RHS. + sequence_expr.expressions.last().and_then(|e| self.get_boolean_value(e)) + } + Expression::UnaryExpression(unary_expr) => { + if unary_expr.operator == UnaryOperator::Void { + Some(false) + } else if matches!( + unary_expr.operator, + UnaryOperator::BitwiseNot + | UnaryOperator::UnaryPlus + | UnaryOperator::UnaryNegation + ) { + // ~0 -> true + // +1 -> true + // +0 -> false + // -0 -> false + self.get_number_value(expr).map(|value| value != NumberValue::Number(0_f64)) + } else if unary_expr.operator == UnaryOperator::LogicalNot { + // !true -> false + self.get_boolean_value(&unary_expr.argument).map(|boolean| !boolean) + } else { + None + } + } + _ => None, + } + } + + /// port from [closure compiler](https://github.com/google/closure-compiler/blob/a4c880032fba961f7a6c06ef99daa3641810bfdd/src/com/google/javascript/jscomp/NodeUtil.java#L348) + /// Gets the value of a node as a Number, or None if it cannot be converted. + /// This method does not consider whether `expr` may have side effects. + fn get_number_value(&self, expr: &Expression) -> Option { + match expr { + Expression::NumericLiteral(number_literal) => { + Some(NumberValue::Number(number_literal.value)) + } + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::UnaryPlus => self.get_number_value(&unary_expr.argument), + UnaryOperator::UnaryNegation => { + self.get_number_value(&unary_expr.argument).map(|v| v.not()) + } + UnaryOperator::BitwiseNot => { + self.get_number_value(&unary_expr.argument).map(|value| { + match value { + NumberValue::Number(num) => NumberValue::Number(f64::from( + !NumericLiteral::ecmascript_to_int32(num), + )), + // ~Infinity -> -1 + // ~-Infinity -> -1 + // ~NaN -> -1 + _ => NumberValue::Number(-1_f64), + } + }) + } + UnaryOperator::LogicalNot => self + .get_boolean_value(expr) + .map(|boolean| if boolean { 1_f64 } else { 0_f64 }) + .map(NumberValue::Number), + UnaryOperator::Void => Some(NumberValue::NaN), + _ => None, + }, + Expression::BooleanLiteral(bool_literal) => { + if bool_literal.value { + Some(NumberValue::Number(1.0)) + } else { + Some(NumberValue::Number(0.0)) + } + } + Expression::NullLiteral(_) => Some(NumberValue::Number(0.0)), + Expression::Identifier(ident) => match ident.name.as_str() { + "Infinity" => Some(NumberValue::PositiveInfinity), + "NaN" | "undefined" => Some(NumberValue::NaN), + _ => None, + }, + // TODO: will be implemented in next PR, just for test pass now. + Expression::StringLiteral(string_literal) => string_literal + .value + .parse::() + .map_or(Some(NumberValue::NaN), |num| Some(NumberValue::Number(num))), + _ => None, + } + } + + #[allow(clippy::cast_possible_truncation)] + fn get_bigint_value(&self, expr: &Expression) -> Option { + match expr { + Expression::NumericLiteral(number_literal) => { + let value = number_literal.value; + if value.abs() < 2_f64.powi(53) && is_exact_int64(value) { + Some(BigInt::from(value as i64)) + } else { + None + } + } + Expression::BigIntLiteral(_bigint_literal) => { + // TODO: evaluate the bigint value + None + } + Expression::BooleanLiteral(bool_literal) => { + if bool_literal.value { + Some(BigInt::one()) + } else { + Some(BigInt::zero()) + } + } + Expression::UnaryExpression(unary_expr) => match unary_expr.operator { + UnaryOperator::LogicalNot => self.get_boolean_value(expr).map(|boolean| { + if boolean { + BigInt::one() + } else { + BigInt::zero() + } + }), + UnaryOperator::UnaryNegation => { + self.get_bigint_value(&unary_expr.argument).map(std::ops::Neg::neg) + } + UnaryOperator::BitwiseNot => { + self.get_bigint_value(&unary_expr.argument).map(std::ops::Not::not) + } + UnaryOperator::UnaryPlus => self.get_bigint_value(&unary_expr.argument), + _ => None, + }, + Expression::StringLiteral(string_literal) => { + self.get_string_bigint_value(&string_literal.value) + } + Expression::TemplateLiteral(_) => { + self.get_string_value(expr).and_then(|value| self.get_string_bigint_value(&value)) + } + _ => None, + } + } + + /// Port from [closure-compiler](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L234) + /// Gets the value of a node as a String, or `None` if it cannot be converted. When it returns a + /// String, this method effectively emulates the `String()` JavaScript cast function. + /// This method does not consider whether `expr` may have side effects. + fn get_string_value<'a>(&self, expr: &'a Expression) -> Option> { + match expr { + Expression::StringLiteral(string_literal) => { + Some(Cow::Borrowed(string_literal.value.as_str())) + } + Expression::TemplateLiteral(template_literal) => { + // TODO: I don't know how to iterate children of TemplateLiteral in order,so only checkout string like `hi`. + // Closure-compiler do more: [case TEMPLATELIT](https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L241-L256). + template_literal + .quasis + .first() + .filter(|quasi| quasi.tail) + .and_then(|quasi| quasi.value.cooked.as_ref()) + .map(|cooked| Cow::Borrowed(cooked.as_str())) + } + Expression::Identifier(ident) => { + let name = ident.name.as_str(); + if matches!(name, "undefined" | "Infinity" | "NaN") { + Some(Cow::Borrowed(name)) + } else { + None + } + } + Expression::NumericLiteral(number_literal) => { + Some(Cow::Owned(number_literal.value.to_string())) + } + Expression::BigIntLiteral(big_int_literal) => { + Some(Cow::Owned(big_int_literal.raw.to_string())) + } + Expression::NullLiteral(_) => Some(Cow::Borrowed("null")), + Expression::BooleanLiteral(bool_literal) => { + if bool_literal.value { + Some(Cow::Borrowed("true")) + } else { + Some(Cow::Borrowed("false")) + } + } + Expression::UnaryExpression(unary_expr) => { + match unary_expr.operator { + UnaryOperator::Void => Some(Cow::Borrowed("undefined")), + UnaryOperator::LogicalNot => { + self.get_boolean_value(&unary_expr.argument).map(|boolean| { + // need reversed. + if boolean { + Cow::Borrowed("false") + } else { + Cow::Borrowed("true") + } + }) + } + _ => None, + } + } + Expression::ArrayExpression(_) => { + // TODO: https://github.com/google/closure-compiler/blob/e13f5cd0a5d3d35f2db1e6c03fdf67ef02946009/src/com/google/javascript/jscomp/NodeUtil.java#L302-L303 + None + } + Expression::ObjectExpression(_) => Some(Cow::Borrowed("[object Object]")), + _ => None, + } + } + + /// port from [closure compiler](https://github.com/google/closure-compiler/blob/master/src/com/google/javascript/jscomp/NodeUtil.java#L540) + fn get_string_bigint_value(&self, raw_string: &str) -> Option { + if raw_string.contains('\u{000b}') { + // vertical tab is not always whitespace + return None; + } + + let s = raw_string.trim(); + + if s.is_empty() { + return Some(BigInt::zero()); + } + + if s.len() > 2 && s.starts_with('0') { + let radix: u32 = match s.chars().nth(1) { + Some('x' | 'X') => 16, + Some('o' | 'O') => 8, + Some('b' | 'B') => 2, + _ => 0, + }; + + if radix == 0 { + return None; + } + + return BigInt::parse_bytes(s[2..].as_bytes(), radix); + } + + return BigInt::parse_bytes(s.as_bytes(), 10); + } +} diff --git a/crates/oxc_minifier/src/node_util/number_value.rs b/crates/oxc_minifier/src/node_util/number_value.rs new file mode 100644 index 0000000000000..e2e1ce8af4ecb --- /dev/null +++ b/crates/oxc_minifier/src/node_util/number_value.rs @@ -0,0 +1,60 @@ +#[derive(PartialEq)] +pub enum NumberValue { + Number(f64), + PositiveInfinity, + NegativeInfinity, + NaN, +} + +impl NumberValue { + #[must_use] + pub fn not(&self) -> Self { + match self { + Self::Number(num) => Self::Number(-num), + Self::PositiveInfinity => Self::NegativeInfinity, + Self::NegativeInfinity => Self::PositiveInfinity, + Self::NaN => Self::NaN, + } + } + + pub fn is_nan(&self) -> bool { + matches!(self, Self::NaN) + } +} + +impl std::ops::Add for NumberValue { + type Output = Self; + + fn add(self, other: Self) -> Self { + match self { + Self::Number(num) => match other { + Self::Number(other_num) => Self::Number(num + other_num), + Self::PositiveInfinity => Self::PositiveInfinity, + Self::NegativeInfinity => Self::NegativeInfinity, + Self::NaN => Self::NaN, + }, + Self::NaN => Self::NaN, + Self::PositiveInfinity => match other { + Self::NaN | Self::NegativeInfinity => Self::NaN, + _ => Self::PositiveInfinity, + }, + Self::NegativeInfinity => match other { + Self::NaN | Self::PositiveInfinity => Self::NaN, + _ => Self::NegativeInfinity, + }, + } + } +} + +impl TryFrom for f64 { + type Error = (); + + fn try_from(value: NumberValue) -> Result { + match value { + NumberValue::Number(num) => Ok(num), + NumberValue::PositiveInfinity => Ok(Self::INFINITY), + NumberValue::NegativeInfinity => Ok(Self::NEG_INFINITY), + NumberValue::NaN => Err(()), + } + } +} diff --git a/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs b/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs index 0c094450cf0fd..b0e8a3fe6bb7e 100644 --- a/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs +++ b/crates/oxc_minifier/tests/ast_passes/remove_dead_code.rs @@ -5,6 +5,10 @@ fn test(source_text: &str, expected: &str) { crate::test(source_text, expected, options); } +fn test_same(source_text: &str) { + test(source_text, source_text); +} + #[test] fn dce_if_statement() { test("if (true) { foo }", "{ foo }"); @@ -52,10 +56,10 @@ fn dce_if_statement() { // Shadowed `undefined` as a variable should not be erased. // This is a rollup test. - test( - "function foo(undefined) { if (!undefined) { } }", - "function foo(undefined) { if (!undefined) { } }", - ); + test_same("function foo(undefined) { if (!undefined) { } }"); + + test("function foo() { if (undefined) { bar } }", "function foo() { }"); + test_same("function foo() { { bar } }"); test("if (true) { foo; } if (true) { foo; }", "{ foo; } { foo; }"); diff --git a/tasks/coverage/minifier_test262.snap b/tasks/coverage/minifier_test262.snap index 66336a0659aac..6f4e52f07c749 100644 --- a/tasks/coverage/minifier_test262.snap +++ b/tasks/coverage/minifier_test262.snap @@ -2,8 +2,4 @@ commit: a1587416 minifier_test262 Summary: AST Parsed : 46406/46406 (100.00%) -Positive Passed: 46402/46406 (99.99%) -Expect to Parse: "language/expressions/logical-and/S11.11.1_A3_T4.js" -Expect to Parse: "language/expressions/logical-not/S9.2_A1_T2.js" -Expect to Parse: "language/statements/if/S12.5_A1.1_T1.js" -Expect to Parse: "language/statements/if/S12.5_A1.1_T2.js" +Positive Passed: 46406/46406 (100.00%)