refactor(minifier): add NodeUtil trait for accessing symbols on ast nodes (#4734)

This commit is contained in:
Boshen 2024-08-08 02:48:24 +00:00
parent f36500a2bd
commit fbfd852cf8
11 changed files with 790 additions and 707 deletions

View file

@ -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<bool> {
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<bool> {
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<Expression<'a>> {
// 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::<f64>::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<Expression<'a>> {
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<Expression<'a>> {
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<Expression<'a>> {
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);
}
_ => {}
};

View file

@ -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

View file

@ -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<Self> 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<NumberValue> for f64 {
type Error = ();
fn try_from(value: NumberValue) -> Result<Self, Self::Error> {
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<BigInt> {
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<NumberValue> {
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::<f64>()
.map_or(Some(NumberValue::NaN), |num| Some(NumberValue::Number(num))),
_ => None,
}
}
#[allow(clippy::cast_possible_truncation)]
pub fn get_bigint_value(expr: &Expression) -> Option<BigInt> {
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<NumberValue> {
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<BigInt> {
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<bool> {
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<Cow<'a, str>> {
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<Cow<'a, str>> {
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
}

View file

@ -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;

View file

@ -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)
}
}
}
}

View file

@ -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),
}
}
}

View file

@ -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> {}

View file

@ -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<NumberValue> {
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<BigInt> {
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<Cow<'a, str>> {
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<bool> {
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<NumberValue> {
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::<f64>()
.map_or(Some(NumberValue::NaN), |num| Some(NumberValue::Number(num))),
_ => None,
}
}
#[allow(clippy::cast_possible_truncation)]
fn get_bigint_value(&self, expr: &Expression) -> Option<BigInt> {
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<Cow<'a, str>> {
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<BigInt> {
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);
}
}

View file

@ -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<Self> 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<NumberValue> for f64 {
type Error = ();
fn try_from(value: NumberValue) -> Result<Self, Self::Error> {
match value {
NumberValue::Number(num) => Ok(num),
NumberValue::PositiveInfinity => Ok(Self::INFINITY),
NumberValue::NegativeInfinity => Ok(Self::NEG_INFINITY),
NumberValue::NaN => Err(()),
}
}
}

View file

@ -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; }");

View file

@ -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%)