mirror of
https://github.com/danbulant/oxc
synced 2026-05-19 12:19:15 +00:00
refactor(minifier): add NodeUtil trait for accessing symbols on ast nodes (#4734)
This commit is contained in:
parent
f36500a2bd
commit
fbfd852cf8
11 changed files with 790 additions and 707 deletions
|
|
@ -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);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
144
crates/oxc_minifier/src/node_util/check_for_state_change.rs
Normal file
144
crates/oxc_minifier/src/node_util/check_for_state_change.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
crates/oxc_minifier/src/node_util/is_literal_value.rs
Normal file
73
crates/oxc_minifier/src/node_util/is_literal_value.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
19
crates/oxc_minifier/src/node_util/may_have_side_effects.rs
Normal file
19
crates/oxc_minifier/src/node_util/may_have_side_effects.rs
Normal 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> {}
|
||||
379
crates/oxc_minifier/src/node_util/mod.rs
Normal file
379
crates/oxc_minifier/src/node_util/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
60
crates/oxc_minifier/src/node_util/number_value.rs
Normal file
60
crates/oxc_minifier/src/node_util/number_value.rs
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }");
|
||||
|
||||
|
|
|
|||
|
|
@ -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%)
|
||||
|
|
|
|||
Loading…
Reference in a new issue