use oxc_allocator::GetAllocator;
use oxc_ast::ast::*;
use oxc_compat::ESFeature;
use oxc_ecmascript::{
GlobalContext,
constant_evaluation::{
ConstantEvaluation, ConstantEvaluationCtx, ConstantValue, ValueType,
binary_operation_evaluate_value,
},
side_effects::{
MayHaveSideEffects, MayHaveSideEffectsContext, PropertyReadSideEffects, is_pure_function,
},
};
use oxc_semantic::{IsGlobalReference, SymbolId};
use oxc_str::format_str;
use oxc_syntax::{reference::ReferenceId, scope::ScopeFlags};
use crate::{
generated::ancestor::Ancestor,
options::CompressOptions,
state::MinifierState,
symbol_value::{FreshValueKind, SymbolValue},
};
use oxc_ast_visit::Visit;
use super::{TraverseCtx, drop_diff::DropDiff};
pub fn is_exact_int64(num: f64) -> bool {
num.fract() == 0.0
}
impl<'a> GlobalContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
ident.is_global_reference(self.scoping())
}
fn get_constant_value_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<ConstantValue<'a>> {
self.tracked_constant_for_reference_id(reference_id).cloned()
}
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
self.tracked_constant_for_reference_id(reference_id).map(ConstantValue::value_type)
}
}
impl<'a> GlobalContext<'a> for &TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
(*self).is_global_reference(ident)
}
fn get_constant_value_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<ConstantValue<'a>> {
(*self).get_constant_value_for_reference_id(reference_id)
}
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
(*self).value_type_for_reference_id(reference_id)
}
}
impl<'a> GlobalContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
fn is_global_reference(&self, ident: &IdentifierReference<'_>) -> bool {
(**self).is_global_reference(ident)
}
fn get_constant_value_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<ConstantValue<'a>> {
(**self).get_constant_value_for_reference_id(reference_id)
}
fn value_type_for_reference_id(&self, reference_id: ReferenceId) -> Option<ValueType> {
(**self).value_type_for_reference_id(reference_id)
}
}
impl<'a> MayHaveSideEffectsContext<'a> for TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
self.state.options.treeshake.annotations
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
let pure_functions = &self.state.options.treeshake.manual_pure_functions;
if pure_functions.is_empty() {
return false;
}
is_pure_function(callee, pure_functions)
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
self.state.options.treeshake.property_read_side_effects
}
fn property_write_side_effects(&self) -> bool {
self.state.options.treeshake.property_write_side_effects
}
fn unknown_global_side_effects(&self) -> bool {
self.state.options.treeshake.unknown_global_side_effects
}
}
impl<'a> MayHaveSideEffectsContext<'a> for &TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
(*self).annotations()
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
(*self).manual_pure_functions(callee)
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
(*self).property_read_side_effects()
}
fn property_write_side_effects(&self) -> bool {
(*self).property_write_side_effects()
}
fn unknown_global_side_effects(&self) -> bool {
(*self).unknown_global_side_effects()
}
}
impl<'a> MayHaveSideEffectsContext<'a> for &mut TraverseCtx<'a, MinifierState<'a>> {
fn annotations(&self) -> bool {
(**self).annotations()
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
(**self).manual_pure_functions(callee)
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
(**self).property_read_side_effects()
}
fn property_write_side_effects(&self) -> bool {
(**self).property_write_side_effects()
}
fn unknown_global_side_effects(&self) -> bool {
(**self).unknown_global_side_effects()
}
}
impl<'a> ConstantEvaluationCtx<'a> for TraverseCtx<'a, MinifierState<'a>> {}
impl<'a> TraverseCtx<'a, MinifierState<'a>> {
pub fn options(&self) -> &CompressOptions {
&self.state.options
}
pub fn supports_feature(&self, feature: ESFeature) -> bool {
!self.options().target.has_feature(feature)
}
pub fn source_type(&self) -> SourceType {
self.state.source_type
}
pub fn is_global_reference(&self, ident: &IdentifierReference<'a>) -> bool {
ident.is_global_reference(self.scoping())
}
fn tracked_constant_for_reference_id(
&self,
reference_id: ReferenceId,
) -> Option<&ConstantValue<'a>> {
self.scoping()
.get_reference(reference_id)
.symbol_id()
.and_then(|symbol_id| self.state.symbol_values.get_symbol_value(symbol_id))
.filter(|sv| sv.write_references_count == 0)
.and_then(|sv| sv.initialized_constant.as_ref())
}
pub fn eval_binary(&self, e: &BinaryExpression<'a>) -> Option<Expression<'a>> {
if e.may_have_side_effects(self) {
None
} else {
e.evaluate_value(self).map(|v| self.value_to_expr(e.span, v))
}
}
pub fn eval_binary_operation(
&self,
operator: BinaryOperator,
left: &Expression<'a>,
right: &Expression<'a>,
) -> Option<ConstantValue<'a>> {
binary_operation_evaluate_value(operator, left, right, self)
}
pub fn value_to_expr(&self, span: Span, value: ConstantValue<'a>) -> Expression<'a> {
match value {
ConstantValue::Number(n) => {
let number_base =
if is_exact_int64(n) { NumberBase::Decimal } else { NumberBase::Float };
Expression::new_numeric_literal(span, n, None, number_base, self)
}
ConstantValue::BigInt(bigint) => {
let value = format_str!(self.allocator(), "{bigint}");
Expression::new_big_int_literal(span, value, None, BigintBase::Decimal, self)
}
ConstantValue::String(s) => {
Expression::new_string_literal(span, Str::from_cow_in(&s, self), None, self)
}
ConstantValue::Boolean(b) => Expression::new_boolean_literal(span, b, self),
ConstantValue::Undefined => Expression::new_void_0(span, self),
ConstantValue::Null => Expression::new_null_literal(span, self),
}
}
pub fn is_expression_undefined(&self, expr: &Expression) -> bool {
match expr {
Expression::Identifier(ident) if self.is_identifier_undefined(ident) => true,
Expression::UnaryExpression(e) if e.operator.is_void() && e.argument.is_number() => {
true
}
_ => false,
}
}
#[inline]
pub fn is_identifier_undefined(&self, ident: &IdentifierReference) -> bool {
if ident.name == "undefined" && ident.is_global_reference(self.scoping()) {
return true;
}
false
}
pub fn init_value(
&mut self,
symbol_id: SymbolId,
constant: Option<ConstantValue<'a>>,
kind: FreshValueKind,
falsy_init: bool,
init_absent: bool,
) {
let mut exported = false;
if self.scoping.current_scope_id() == self.scoping().root_scope_id() {
for ancestor in self.ancestors() {
if ancestor.is_export_named_declaration()
|| ancestor.is_export_all_declaration()
|| ancestor.is_export_default_declaration()
{
exported = true;
}
}
}
let mut read_references_count = 0;
let mut write_references_count = 0;
let mut member_write_target_read_count = 0;
for r in self.scoping().get_resolved_references(symbol_id) {
if r.is_read() {
read_references_count += 1;
}
if r.is_write() {
write_references_count += 1;
}
if r.flags().is_member_write_target() {
member_write_target_read_count += 1;
}
}
let scope_id = self.scoping().symbol_scope_id(symbol_id);
let scope_flags = self.scoping().scope_flags(scope_id);
let value_withheld = constant.is_none();
let initialized_constant =
if scope_flags.contains(ScopeFlags::DirectEval) { None } else { constant };
let boolean_falsy = falsy_init
&& value_withheld
&& write_references_count == 0
&& !scope_flags.contains(ScopeFlags::DirectEval)
&& !(self.source_type().is_script() && scope_id == self.scoping().root_scope_id());
let implicit_undefined =
init_absent && initialized_constant.as_ref().is_some_and(ConstantValue::is_undefined);
let symbol_value = SymbolValue {
initialized_constant,
implicit_undefined,
exported,
read_references_count,
write_references_count,
member_write_target_read_count,
kind,
boolean_falsy,
};
self.state.symbol_values.init_value(symbol_id, symbol_value);
}
pub fn expr_eq(&self, a: &Expression<'a>, b: &Expression<'a>) -> bool {
use oxc_span::ContentEq;
a.content_eq(b) || (self.is_expression_undefined(a) && self.is_expression_undefined(b))
}
pub fn string_to_equivalent_number_value(s: &str) -> Option<f64> {
if s.is_empty() {
return None;
}
let mut is_negative = false;
let mut int_value = 0i32;
let mut start = 0;
let bytes = s.as_bytes();
if bytes[0] == b'-' && s.len() > 1 {
is_negative = true;
int_value = -int_value;
start += 1;
}
if bytes[start] == b'0' && s.len() > 1 {
return None;
}
for b in &bytes[start..] {
if !b.is_ascii_digit() {
return None;
}
int_value = int_value.checked_mul(10).and_then(|v| {
let n = i32::from(b & 15);
if is_negative { v.checked_sub(n) } else { v.checked_add(n) }
})?;
}
Some(f64::from(int_value))
}
pub fn is_closest_function_scope_an_async_generator(&self) -> bool {
self.ancestors()
.find_map(|ancestor| match ancestor {
Ancestor::FunctionBody(body) => Some(*body.r#async() && *body.generator()),
Ancestor::ArrowFunctionExpressionBody(_) => Some(false),
_ => None,
})
.unwrap_or_default()
}
pub fn is_expression_whose_name_needs_to_be_kept(&self, expr: &Expression) -> bool {
let options = &self.options().keep_names;
if !options.class && !options.function {
return false;
}
if !expr.is_anonymous_function_definition() {
return false;
}
let is_class = matches!(expr.without_parentheses(), Expression::ClassExpression(_));
(options.class && is_class) || (options.function && !is_class)
}
#[inline]
fn dirty_diff(&mut self) -> DropDiff<'a, '_> {
DropDiff::new(&mut self.state.dirty)
}
#[inline]
pub fn replace_expression(&mut self, slot: &mut Expression<'a>, new: Expression<'a>) {
self.dirty_diff().visit_expression(slot);
*slot = new;
self.state.record_mutation();
}
#[inline]
pub fn replace_statement(&mut self, slot: &mut Statement<'a>, new: Statement<'a>) {
self.dirty_diff().visit_statement(slot);
*slot = new;
self.state.record_mutation();
}
#[inline]
pub fn replace_assignment_target_property(
&mut self,
slot: &mut AssignmentTargetProperty<'a>,
new: AssignmentTargetProperty<'a>,
) {
self.dirty_diff().visit_assignment_target_property(slot);
*slot = new;
self.state.record_mutation();
}
#[inline]
pub fn replace_property_key(&mut self, slot: &mut PropertyKey<'a>, new: PropertyKey<'a>) {
self.dirty_diff().visit_property_key(slot);
*slot = new;
self.state.record_mutation();
}
#[inline]
pub fn replace_for_statement_left(
&mut self,
slot: &mut ForStatementLeft<'a>,
new: ForStatementLeft<'a>,
) {
self.dirty_diff().visit_for_statement_left(slot);
*slot = new;
self.state.record_mutation();
}
#[inline]
pub fn notice_change(&mut self) {
self.state.record_mutation();
}
#[inline]
pub fn drop_expression(&mut self, expr: &Expression<'a>) {
self.dirty_diff().visit_expression(expr);
self.state.record_mutation();
}
#[inline]
pub fn drop_statement(&mut self, stmt: &Statement<'a>) {
self.dirty_diff().visit_statement(stmt);
self.state.record_mutation();
}
#[inline]
pub fn drop_class_element(&mut self, element: &ClassElement<'a>) {
self.dirty_diff().visit_class_element(element);
self.state.record_mutation();
}
#[inline]
pub fn drop_variable_declarator(&mut self, decl: &VariableDeclarator<'a>) {
self.dirty_diff().visit_variable_declarator(decl);
self.state.record_mutation();
}
}