use std::ops::{BitOr, BitOrAssign};
use bitflags::bitflags;
use oxc::ast::ast::{
self, Argument, AssignmentTarget, BindingPattern, CallExpression, ChainElement, Expression,
IdentifierReference, SpreadElement, UnaryOperator, VariableDeclarationKind,
};
use oxc::ast::match_member_expression;
use oxc::ast_visit::{VisitJs, walk_js};
use oxc::semantic::{NodeId, ScopeFlags, SymbolId};
use oxc::transformer::EngineTargets;
use oxc_ecmascript::GlobalContext;
use oxc_ecmascript::side_effects::{
MayHaveSideEffects, MayHaveSideEffectsContext, PropertyReadSideEffects,
};
use rolldown_common::{AstScopes, FlatOptions, SharedNormalizedBundlerOptions, StmtEvalFlags};
use rolldown_ecmascript_utils::ExpressionExt;
use rustc_hash::FxHashSet;
use crate::utils;
bitflags! {
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
struct StmtOrderSensitiveReasons: u8 {
const GlobalVarAccess = 1;
const PureAnnotation = 1 << 1;
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Default)]
pub struct StmtEvalFacts {
tree_shaking_flags: StmtEvalFlags,
order_sensitive_reasons: StmtOrderSensitiveReasons,
}
impl StmtEvalFacts {
#[inline]
fn from_tree_shaking_flags(tree_shaking_flags: StmtEvalFlags) -> Self {
Self { tree_shaking_flags, order_sensitive_reasons: StmtOrderSensitiveReasons::empty() }
}
#[inline]
fn from_tree_shaking_side_effect(has_side_effect: bool) -> Self {
Self::from_tree_shaking_flags(has_side_effect.into())
}
#[inline]
fn from_unknown_side_effect() -> Self {
Self::from_tree_shaking_flags(StmtEvalFlags::UnknownSideEffect)
}
#[inline]
pub fn tree_shaking_flags(&self) -> StmtEvalFlags {
self.tree_shaking_flags
}
#[inline]
fn set_order_sensitive_reason(&mut self, reason: StmtOrderSensitiveReasons, value: bool) {
self.order_sensitive_reasons.set(reason, value);
}
#[inline]
fn has_order_sensitive_reason(&self, reason: StmtOrderSensitiveReasons) -> bool {
self.order_sensitive_reasons.contains(reason)
}
#[inline]
fn without_unknown_side_effect(mut self) -> Self {
self.tree_shaking_flags.remove(StmtEvalFlags::UnknownSideEffect);
self
}
#[inline]
fn has_side_effect_for_tree_shaking(&self) -> bool {
self.tree_shaking_flags.has_side_effect_for_tree_shaking()
}
#[inline]
pub fn is_order_sensitive(&self) -> bool {
self.tree_shaking_flags.contains(StmtEvalFlags::UnknownSideEffect)
|| !self.order_sensitive_reasons.is_empty()
}
}
impl BitOr for StmtEvalFacts {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self {
tree_shaking_flags: self.tree_shaking_flags | rhs.tree_shaking_flags,
order_sensitive_reasons: self.order_sensitive_reasons | rhs.order_sensitive_reasons,
}
}
}
impl BitOrAssign for StmtEvalFacts {
fn bitor_assign(&mut self, rhs: Self) {
self.tree_shaking_flags |= rhs.tree_shaking_flags;
self.order_sensitive_reasons |= rhs.order_sensitive_reasons;
}
}
pub struct StmtEvalAnalyzer<'a> {
scope: &'a AstScopes,
options: &'a SharedNormalizedBundlerOptions,
flat_options: FlatOptions,
side_effect_free_call_expr_node_ids: Option<&'a FxHashSet<NodeId>>,
namespace_object_symbol_ids: Option<&'a FxHashSet<SymbolId>>,
}
impl<'a> StmtEvalAnalyzer<'a> {
pub fn new(
scope: &'a AstScopes,
flat_options: FlatOptions,
options: &'a SharedNormalizedBundlerOptions,
side_effect_free_call_expr_node_ids: Option<&'a FxHashSet<NodeId>>,
namespace_object_symbol_ids: Option<&'a FxHashSet<SymbolId>>,
) -> Self {
Self {
scope,
options,
flat_options,
side_effect_free_call_expr_node_ids,
namespace_object_symbol_ids,
}
}
fn is_call_expr_marked_pure(&self, expr: &CallExpression) -> bool {
self.side_effect_free_call_expr_node_ids.is_some_and(|set| set.contains(&expr.node_id()))
}
#[inline]
fn is_unresolved_reference(&self, ident_ref: &IdentifierReference) -> bool {
ident_ref.reference_id.get().is_some_and(|ref_id| self.scope.is_unresolved(ref_id))
}
fn is_member_expr_root_global(&self, expr: &ast::MemberExpression) -> bool {
let mut cur = expr.object();
loop {
match cur {
Expression::StaticMemberExpression(e) => cur = &e.object,
Expression::ComputedMemberExpression(e) => cur = &e.object,
Expression::Identifier(ident) => return self.is_unresolved_reference(ident),
_ => return false,
}
}
}
fn is_namespace_member_access(&self, member_expr: &ast::MemberExpression) -> Option<bool> {
let namespace_ids = self.namespace_object_symbol_ids?;
let ident = member_expr.object().as_identifier()?;
let ref_id = ident.reference_id.get()?;
let symbol_id = self.scope.symbol_id_for(ref_id)?;
Some(namespace_ids.contains(&symbol_id))
}
fn is_side_effect_free_import_meta_access(member_expr: &ast::MemberExpression) -> bool {
let Expression::ImportMeta(_) = member_expr.object() else {
return false;
};
member_expr
.static_property_name()
.is_some_and(|name| name == "url" || utils::file_url::starts_with_file_url_prefix(name))
}
fn analyze_member_expr(&self, member_expr: &ast::MemberExpression) -> StmtEvalFacts {
if self.is_expr_manual_pure_functions(member_expr.object()) {
let mut facts = self.analyze_eager_chain_children(member_expr.object());
if let ast::MemberExpression::ComputedMemberExpression(expr) = member_expr {
facts |= self.analyze_expr(&expr.expression);
}
return facts;
}
if self.is_namespace_member_access(member_expr) == Some(true) {
return match member_expr {
ast::MemberExpression::ComputedMemberExpression(e) => self.analyze_expr(&e.expression),
_ => StmtEvalFacts::default(),
};
}
if Self::is_side_effect_free_import_meta_access(member_expr) {
return StmtEvalFacts::default();
}
let is_global = self.is_member_expr_root_global(member_expr);
let has_side_effect = member_expr.may_have_side_effects(self);
let mut facts = StmtEvalFacts::from_tree_shaking_side_effect(has_side_effect);
facts.set_order_sensitive_reason(StmtOrderSensitiveReasons::GlobalVarAccess, is_global);
facts
}
fn analyze_eager_chain_children(&self, expr: &Expression) -> StmtEvalFacts {
let mut facts = StmtEvalFacts::default();
for_each_eager_chain_child(expr, |child| {
facts |= match child {
EagerChild::Expr(expr) => self.analyze_expr(expr),
EagerChild::Spread(spread) => self.analyze_spread_argument(spread),
};
});
facts
}
fn analyze_spread_argument(&self, spread: &SpreadElement) -> StmtEvalFacts {
match &spread.argument {
Expression::ArrayExpression(_)
| Expression::StringLiteral(_)
| Expression::TemplateLiteral(_) => self.analyze_expr(&spread.argument),
_ => StmtEvalFacts::from_unknown_side_effect(),
}
}
fn analyze_side_effect_free_write_target(
&self,
target: &ast::SimpleAssignmentTarget,
) -> StmtEvalFacts {
let object_detail = match target {
ast::SimpleAssignmentTarget::StaticMemberExpression(e) => self.analyze_expr(&e.object),
ast::SimpleAssignmentTarget::ComputedMemberExpression(e) => {
self.analyze_expr(&e.object) | self.analyze_expr(&e.expression)
}
ast::SimpleAssignmentTarget::PrivateFieldExpression(e) => self.analyze_expr(&e.object),
_ => return StmtEvalFacts::default(),
};
if object_detail.has_order_sensitive_reason(StmtOrderSensitiveReasons::GlobalVarAccess) {
object_detail | StmtEvalFacts::from_unknown_side_effect()
} else {
object_detail
}
}
fn analyze_call_expr(&self, expr: &CallExpression) -> StmtEvalFacts {
let is_pure_annotated =
!self.flat_options.ignore_annotations() && (expr.pure || self.is_call_expr_marked_pure(expr));
let has_side_effect = if is_pure_annotated { false } else { expr.may_have_side_effects(self) };
let is_global_call = !has_side_effect
&& matches!(&expr.callee, Expression::Identifier(id) if self.is_unresolved_reference(id));
let mut facts = StmtEvalFacts::from_tree_shaking_side_effect(has_side_effect);
facts.set_order_sensitive_reason(StmtOrderSensitiveReasons::PureAnnotation, is_pure_annotated);
facts.set_order_sensitive_reason(StmtOrderSensitiveReasons::GlobalVarAccess, is_global_call);
if !has_side_effect {
facts |= self.analyze_expr(&expr.callee).without_unknown_side_effect();
if is_pure_annotated {
for arg in &expr.arguments {
facts |= match arg {
Argument::SpreadElement(_) => StmtEvalFacts::from_unknown_side_effect(),
_ => self.analyze_expr(arg.to_expression()),
};
if facts.has_side_effect_for_tree_shaking() {
break;
}
}
} else {
for arg in &expr.arguments {
if let Argument::SpreadElement(_) = arg {
break;
}
facts |= self.analyze_expr(arg.to_expression()).without_unknown_side_effect();
}
}
}
facts
}
fn is_expr_manual_pure_functions(&self, expr: &Expression) -> bool {
if self.flat_options.is_manual_pure_functions_empty() {
return false;
}
let manual_pure_functions = self.options.treeshake.manual_pure_functions().unwrap();
chain_root_ident(expr).is_some_and(|first| manual_pure_functions.contains(first))
}
fn analyze_expr(&self, expr: &Expression) -> StmtEvalFacts {
let expr = expr.get_inner_expression();
match expr {
oxc::ast::match_member_expression!(Expression) => {
self.analyze_member_expr(expr.to_member_expression())
}
Expression::Identifier(ident) => self.analyze_identifier(ident),
Expression::AssignmentExpression(assign_expr) => {
if let Some(pure_cjs) = check_pure_cjs_export(self.scope, &assign_expr.left) {
return StmtEvalFacts::from_tree_shaking_flags(pure_cjs)
| self.analyze_expr(&assign_expr.right);
}
if assign_expr.may_have_side_effects(self) {
return StmtEvalFacts::from_unknown_side_effect();
}
self.analyze_side_effect_free_write_target(assign_expr.left.to_simple_assignment_target())
}
Expression::ChainExpression(chain_expr) => match &chain_expr.expression {
ChainElement::CallExpression(call_expr) => self.analyze_call_expr(call_expr),
ChainElement::TSNonNullExpression(ts_expr) => self.analyze_expr(&ts_expr.expression),
match_member_expression!(ChainElement) => {
self.analyze_member_expr(chain_expr.expression.to_member_expression())
}
},
Expression::UpdateExpression(update_expr) => {
if update_expr.may_have_side_effects(self) {
return StmtEvalFacts::from_unknown_side_effect();
}
self.analyze_side_effect_free_write_target(&update_expr.argument)
}
Expression::NewExpression(expr) => {
let has_side_effect = expr.may_have_side_effects(self);
let is_global_constructor = !has_side_effect
&& matches!(&expr.callee, Expression::Identifier(id) if self.is_unresolved_reference(id));
let is_pure_annotated = !self.flat_options.ignore_annotations() && expr.pure;
let mut facts = StmtEvalFacts::from_tree_shaking_side_effect(has_side_effect);
facts.set_order_sensitive_reason(
StmtOrderSensitiveReasons::GlobalVarAccess,
is_global_constructor,
);
facts
.set_order_sensitive_reason(StmtOrderSensitiveReasons::PureAnnotation, is_pure_annotated);
if !has_side_effect {
for arg in &expr.arguments {
if let Argument::SpreadElement(_) = arg {
break;
}
facts |= self.analyze_expr(arg.to_expression()).without_unknown_side_effect();
}
}
facts
}
Expression::CallExpression(expr) => self.analyze_call_expr(expr),
Expression::ObjectExpression(obj) => self.fold_compound(
expr,
obj
.properties
.iter()
.flat_map(|prop| match prop {
ast::ObjectPropertyKind::ObjectProperty(p) => {
[p.computed.then(|| p.key.to_expression()), Some(&p.value)]
}
ast::ObjectPropertyKind::SpreadProperty(s) => [Some(&s.argument), None],
})
.flatten(),
),
Expression::ArrayExpression(arr) => self.fold_compound(
expr,
arr.elements.iter().filter_map(|elem| match elem {
ast::ArrayExpressionElement::SpreadElement(s) => Some(&s.argument),
ast::ArrayExpressionElement::Elision(_) => None,
e => Some(e.to_expression()),
}),
),
Expression::SequenceExpression(s) => self.fold_compound(expr, &s.expressions),
Expression::TemplateLiteral(t) => self.fold_compound(expr, &t.expressions),
Expression::ConditionalExpression(c) => {
self.fold_compound(expr, [&c.test, &c.consequent, &c.alternate])
}
Expression::LogicalExpression(e) => self.fold_compound(expr, [&e.left, &e.right]),
Expression::BinaryExpression(e) => self.fold_compound(expr, [&e.left, &e.right]),
Expression::UnaryExpression(e) => self.fold_compound(expr, [&e.argument]),
Expression::ClassExpression(class) => {
self.analyze_class_definition(expr.may_have_side_effects(self), class)
}
_ => StmtEvalFacts::from_tree_shaking_side_effect(expr.may_have_side_effects(self)),
}
}
fn fold_compound<'e>(
&self,
expr: &Expression,
children: impl IntoIterator<Item = &'e Expression<'e>>,
) -> StmtEvalFacts {
if expr.may_have_side_effects(self) {
return StmtEvalFacts::from_unknown_side_effect();
}
let mut facts = StmtEvalFacts::default();
for child in children {
facts |= self.analyze_expr(child);
if facts.has_side_effect_for_tree_shaking() {
break;
}
}
facts.without_unknown_side_effect()
}
fn analyze_var_decl(&self, var_decl: &ast::VariableDeclaration) -> StmtEvalFacts {
match var_decl.kind {
VariableDeclarationKind::AwaitUsing => StmtEvalFacts::from_unknown_side_effect(),
VariableDeclarationKind::Using => self.analyze_using_declarators(&var_decl.declarations),
_ => {
let mut facts = StmtEvalFacts::default();
for declarator in &var_decl.declarations {
facts |= match &declarator.id {
BindingPattern::ObjectPattern(_) if self.flat_options.property_read_side_effects() => {
StmtEvalFacts::from_unknown_side_effect()
}
BindingPattern::ArrayPattern(pat)
if pat.elements.iter().any(|p| {
p.as_ref().is_some_and(|pat| !matches!(pat, BindingPattern::BindingIdentifier(_)))
}) =>
{
StmtEvalFacts::from_unknown_side_effect()
}
_ => declarator.init.as_ref().map(|init| self.analyze_expr(init)).unwrap_or_default(),
};
}
facts
}
}
}
fn analyze_class_definition(&self, has_side_effect: bool, class: &ast::Class) -> StmtEvalFacts {
let mut facts = StmtEvalFacts::from_tree_shaking_side_effect(has_side_effect);
if !has_side_effect {
facts.order_sensitive_reasons |=
EagerEvaluationOrderReasonCollector::collect_class(self, class);
}
facts
}
pub(crate) fn add_top_level_eager_order_reasons(
&self,
stmt: &ast::Statement,
facts: &mut StmtEvalFacts,
) {
debug_assert!(!facts.is_order_sensitive());
facts.order_sensitive_reasons |=
EagerEvaluationOrderReasonCollector::collect_statement(self, stmt);
}
fn analyze_decl(&self, decl: &ast::Declaration) -> StmtEvalFacts {
match decl {
ast::Declaration::VariableDeclaration(var_decl) => self.analyze_var_decl(var_decl),
ast::Declaration::ClassDeclaration(class) => {
self.analyze_class_definition(decl.may_have_side_effects(self), class)
}
_ => StmtEvalFacts::from_tree_shaking_side_effect(decl.may_have_side_effects(self)),
}
}
fn analyze_using_declarators(&self, declarators: &[ast::VariableDeclarator]) -> StmtEvalFacts {
let mut facts = StmtEvalFacts::default();
for decl in declarators {
facts |= decl
.init
.as_ref()
.map(|init| match init {
Expression::NullLiteral(_) => StmtEvalFacts::default(),
Expression::Identifier(id) => StmtEvalFacts::from_tree_shaking_side_effect(
!(id.name == "undefined" && self.is_unresolved_reference(id)),
),
Expression::UnaryExpression(expr) if matches!(expr.operator, UnaryOperator::Void) => {
self.analyze_expr(&expr.argument)
}
_ => StmtEvalFacts::from_unknown_side_effect(),
})
.unwrap_or_default();
if facts.has_side_effect_for_tree_shaking() {
break;
}
}
facts
}
fn analyze_identifier(&self, ident_ref: &IdentifierReference) -> StmtEvalFacts {
let is_global = self.is_unresolved_reference(ident_ref);
let has_side_effect = ident_ref.may_have_side_effects(self);
let mut facts = StmtEvalFacts::from_tree_shaking_side_effect(has_side_effect);
facts.set_order_sensitive_reason(StmtOrderSensitiveReasons::GlobalVarAccess, is_global);
facts
}
fn analyze_module_declaration(&self, decl: &ast::ModuleDeclaration) -> StmtEvalFacts {
match decl {
ast::ModuleDeclaration::ExportAllDeclaration(_)
| ast::ModuleDeclaration::ExportFromDeclaration(_)
| ast::ModuleDeclaration::ExportNamedDeclaration(_)
| ast::ModuleDeclaration::ImportDeclaration(_) => {
StmtEvalFacts::default()
}
ast::ModuleDeclaration::ExportDefaultDeclaration(default_decl) => {
use oxc::ast::ast::ExportDefaultDeclarationKind;
match &default_decl.declaration {
decl @ oxc::ast::match_expression!(ExportDefaultDeclarationKind) => {
self.analyze_expr(decl.to_expression())
}
ast::ExportDefaultDeclarationKind::FunctionDeclaration(_) => StmtEvalFacts::default(),
ast::ExportDefaultDeclarationKind::ClassDeclaration(class) => {
self.analyze_class_definition(class.may_have_side_effects(self), class)
}
ast::ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => {
StmtEvalFacts::from_unknown_side_effect()
}
}
}
ast::ModuleDeclaration::ExportDeclaration(export_decl) => {
self.analyze_decl(&export_decl.declaration)
}
ast::ModuleDeclaration::TSExportAssignment(_)
| ast::ModuleDeclaration::TSNamespaceExportDeclaration(_) => {
StmtEvalFacts::from_unknown_side_effect()
}
}
}
pub fn analyze_stmt(&self, stmt: &ast::Statement) -> StmtEvalFacts {
use oxc::ast::ast::Statement;
match stmt {
oxc::ast::match_module_declaration!(Statement) => {
self.analyze_module_declaration(stmt.to_module_declaration())
}
oxc::ast::match_declaration!(Statement) => self.analyze_decl(stmt.to_declaration()),
Statement::ExpressionStatement(expr) => self.analyze_expr(&expr.expression),
Statement::BlockStatement(block) => self.analyze_block(block),
Statement::DoWhileStatement(do_while) => {
self.analyze_stmt(&do_while.body) | self.analyze_expr(&do_while.test)
}
Statement::WhileStatement(while_stmt) => {
self.analyze_expr(&while_stmt.test) | self.analyze_stmt(&while_stmt.body)
}
Statement::IfStatement(if_stmt) => {
self.analyze_expr(&if_stmt.test)
| self.analyze_stmt(&if_stmt.consequent)
| if_stmt.alternate.as_ref().map(|s| self.analyze_stmt(s)).unwrap_or_default()
}
Statement::ReturnStatement(ret_stmt) => {
ret_stmt.argument.as_ref().map(|expr| self.analyze_expr(expr)).unwrap_or_default()
}
Statement::LabeledStatement(labeled_stmt) => self.analyze_stmt(&labeled_stmt.body),
Statement::TryStatement(try_stmt) => {
let mut facts = self.analyze_block(&try_stmt.block);
facts |= try_stmt
.handler
.as_ref()
.map(|handler| self.analyze_block(&handler.body))
.unwrap_or_default();
facts |= try_stmt
.finalizer
.as_ref()
.map(|finalizer| self.analyze_block(finalizer))
.unwrap_or_default();
facts
}
Statement::SwitchStatement(switch_stmt) => {
let mut facts = self.analyze_expr(&switch_stmt.discriminant);
if facts.has_side_effect_for_tree_shaking() {
return facts;
}
'outer: for case in &switch_stmt.cases {
facts |= case.test.as_ref().map(|expr| self.analyze_expr(expr)).unwrap_or_default();
for stmt in &case.consequent {
facts |= self.analyze_stmt(stmt);
if facts.has_side_effect_for_tree_shaking() {
break 'outer;
}
}
}
facts
}
_ => StmtEvalFacts::from_tree_shaking_side_effect(stmt.may_have_side_effects(self)),
}
}
fn analyze_block(&self, block: &ast::BlockStatement) -> StmtEvalFacts {
let mut facts = StmtEvalFacts::default();
for stmt in &block.body {
facts |= self.analyze_stmt(stmt);
if facts.has_side_effect_for_tree_shaking() {
break;
}
}
facts
}
}
struct EagerEvaluationOrderReasonCollector<'analyzer, 'ctx> {
analyzer: &'analyzer StmtEvalAnalyzer<'ctx>,
reasons: StmtOrderSensitiveReasons,
extended_top_level_reasons: bool,
}
impl<'analyzer, 'ctx> EagerEvaluationOrderReasonCollector<'analyzer, 'ctx> {
fn new(analyzer: &'analyzer StmtEvalAnalyzer<'ctx>, extended_top_level_reasons: bool) -> Self {
Self { analyzer, reasons: StmtOrderSensitiveReasons::empty(), extended_top_level_reasons }
}
fn collect_statement(
analyzer: &'analyzer StmtEvalAnalyzer<'ctx>,
stmt: &ast::Statement,
) -> StmtOrderSensitiveReasons {
let mut collector = Self::new(analyzer, true);
collector.visit_statement(stmt);
collector.reasons
}
fn collect_class(
analyzer: &'analyzer StmtEvalAnalyzer<'ctx>,
class: &ast::Class,
) -> StmtOrderSensitiveReasons {
let mut collector = Self::new(analyzer, false);
collector.visit_class(class);
collector.reasons
}
fn add_reason_if(&mut self, reason: StmtOrderSensitiveReasons, value: bool) {
if value {
self.reasons.insert(reason);
}
}
fn visit_immediately_invoked_function(&mut self, callee: &Expression<'_>) {
match callee.get_inner_expression() {
Expression::FunctionExpression(function) => {
self.visit_formal_parameters(&function.params);
if !function.generator
&& let Some(body) = &function.body
{
self.visit_function_body(body);
}
}
Expression::ArrowFunctionExpression(arrow) => {
self.visit_formal_parameters(&arrow.params);
self.visit_arrow_function_body(&arrow.body);
}
Expression::SequenceExpression(sequence) => {
if let Some(last) = sequence.expressions.last() {
self.visit_immediately_invoked_function(last);
}
}
Expression::ConditionalExpression(conditional) if self.extended_top_level_reasons => {
self.visit_immediately_invoked_function(&conditional.consequent);
self.visit_immediately_invoked_function(&conditional.alternate);
}
Expression::LogicalExpression(logical) if self.extended_top_level_reasons => {
self.visit_immediately_invoked_function(&logical.left);
self.visit_immediately_invoked_function(&logical.right);
}
Expression::AssignmentExpression(assignment) if self.extended_top_level_reasons => {
self.visit_immediately_invoked_function(&assignment.right);
}
Expression::ClassExpression(class) if self.extended_top_level_reasons => {
self.visit_constructed_class_positions(class);
}
_ => {}
}
}
fn visit_constructed_class_positions(&mut self, class: &ast::Class<'_>) {
for element in &class.body.body {
match element {
ast::ClassElement::MethodDefinition(method)
if matches!(method.kind, ast::MethodDefinitionKind::Constructor) =>
{
self.visit_formal_parameters(&method.value.params);
if let Some(body) = &method.value.body {
self.visit_function_body(body);
}
}
ast::ClassElement::PropertyDefinition(property) if !property.r#static => {
if let Some(value) = &property.value {
self.visit_expression(value);
}
}
ast::ClassElement::AccessorProperty(accessor) if !accessor.r#static => {
if let Some(value) = &accessor.value {
self.visit_expression(value);
}
}
_ => {}
}
}
}
fn visit_class_definition(&mut self, class: &ast::Class<'_>) {
for decorator in &class.decorators {
self.visit_expression(&decorator.expression);
}
if let Some(super_class) = &class.super_class {
self.visit_expression(super_class);
}
for element in &class.body.body {
match element {
ast::ClassElement::StaticBlock(block) => {
for stmt in &block.body {
self.visit_statement(stmt);
}
}
ast::ClassElement::MethodDefinition(method) => {
for decorator in &method.decorators {
self.visit_expression(&decorator.expression);
}
if method.computed {
self.visit_expression(method.key.to_expression());
}
}
ast::ClassElement::PropertyDefinition(property) => {
for decorator in &property.decorators {
self.visit_expression(&decorator.expression);
}
if property.computed {
self.visit_expression(property.key.to_expression());
}
if property.r#static
&& let Some(value) = &property.value
{
self.visit_expression(value);
}
}
ast::ClassElement::AccessorProperty(accessor) => {
for decorator in &accessor.decorators {
self.visit_expression(&decorator.expression);
}
if accessor.computed {
self.visit_expression(accessor.key.to_expression());
}
if accessor.r#static
&& let Some(value) = &accessor.value
{
self.visit_expression(value);
}
}
ast::ClassElement::TSIndexSignature(_) => {}
}
}
}
}
impl<'ast> VisitJs<'ast> for EagerEvaluationOrderReasonCollector<'_, '_> {
fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'ast>) {
let is_global_read = ident.reference_id.get().is_some_and(|reference_id| {
let reference = self.analyzer.scope.scoping().get_reference(reference_id);
reference.symbol_id().is_none() && reference.is_read()
});
self.add_reason_if(StmtOrderSensitiveReasons::GlobalVarAccess, is_global_read);
}
fn visit_assignment_expression(&mut self, assignment: &ast::AssignmentExpression<'ast>) {
if check_pure_cjs_export(self.analyzer.scope, &assignment.left).is_some() {
self.visit_expression(&assignment.right);
} else {
walk_js::walk_assignment_expression(self, assignment);
}
}
fn visit_call_expression(&mut self, call: &CallExpression<'ast>) {
let is_pure_annotated = !self.analyzer.flat_options.ignore_annotations()
&& (call.pure || self.analyzer.is_call_expr_marked_pure(call));
let is_manual_pure =
self.extended_top_level_reasons && self.analyzer.is_expr_manual_pure_functions(&call.callee);
self.add_reason_if(
StmtOrderSensitiveReasons::PureAnnotation,
is_pure_annotated || is_manual_pure,
);
walk_js::walk_call_expression(self, call);
self.visit_immediately_invoked_function(&call.callee);
}
fn visit_new_expression(&mut self, new_expr: &ast::NewExpression<'ast>) {
self.add_reason_if(
StmtOrderSensitiveReasons::PureAnnotation,
(!self.analyzer.flat_options.ignore_annotations() && new_expr.pure)
|| (self.extended_top_level_reasons
&& self.analyzer.is_expr_manual_pure_functions(&new_expr.callee)),
);
walk_js::walk_new_expression(self, new_expr);
self.visit_immediately_invoked_function(&new_expr.callee);
}
fn visit_tagged_template_expression(&mut self, tagged: &ast::TaggedTemplateExpression<'ast>) {
self.add_reason_if(
StmtOrderSensitiveReasons::PureAnnotation,
self.extended_top_level_reasons && self.analyzer.is_expr_manual_pure_functions(&tagged.tag),
);
walk_js::walk_tagged_template_expression(self, tagged);
if self.extended_top_level_reasons {
self.visit_immediately_invoked_function(&tagged.tag);
}
}
fn visit_function(&mut self, _function: &ast::Function<'ast>, _flags: ScopeFlags) {}
fn visit_arrow_function_expression(&mut self, _arrow: &ast::ArrowFunctionExpression<'ast>) {}
fn visit_class(&mut self, class: &ast::Class<'ast>) {
self.visit_class_definition(class);
}
}
fn check_pure_cjs_export(scope: &AstScopes, target: &AssignmentTarget) -> Option<StmtEvalFlags> {
match target {
AssignmentTarget::ComputedMemberExpression(_) | AssignmentTarget::StaticMemberExpression(_) => {
let member_expr = target.to_member_expression();
if let Expression::Identifier(ident) = member_expr.object() {
if ident.name == "exports"
&& member_expr.static_property_name().is_some()
&& ident.reference_id.get().is_some_and(|ref_id| scope.is_unresolved(ref_id))
{
return Some(StmtEvalFlags::PureCjs);
}
}
None
}
_ => None,
}
}
enum EagerChild<'a> {
Expr(&'a Expression<'a>),
Spread(&'a SpreadElement<'a>),
}
fn chain_root_visiting_eager_children<'a>(
expr: &'a Expression,
mut on_eager_child: impl FnMut(EagerChild<'a>),
) -> Option<&'a str> {
fn visit_call_args<'a>(
args: &'a [Argument<'a>],
on_eager_child: &mut impl FnMut(EagerChild<'a>),
) {
for arg in args {
on_eager_child(match arg {
Argument::SpreadElement(spread) => EagerChild::Spread(spread),
_ => EagerChild::Expr(arg.to_expression()),
});
}
}
let mut cur = expr;
loop {
match cur {
Expression::Identifier(ident) => break Some(ident.name.as_str()),
Expression::ComputedMemberExpression(expr) => {
on_eager_child(EagerChild::Expr(&expr.expression));
cur = &expr.object;
}
Expression::StaticMemberExpression(expr) => {
cur = &expr.object;
}
Expression::CallExpression(expr) => {
visit_call_args(&expr.arguments, &mut on_eager_child);
cur = &expr.callee;
}
Expression::ChainExpression(expr) => match expr.expression {
ChainElement::CallExpression(ref call_expression) => {
visit_call_args(&call_expression.arguments, &mut on_eager_child);
cur = &call_expression.callee;
}
ChainElement::ComputedMemberExpression(ref computed_member_expression) => {
on_eager_child(EagerChild::Expr(&computed_member_expression.expression));
cur = &computed_member_expression.object;
}
ChainElement::StaticMemberExpression(ref static_member_expression) => {
cur = &static_member_expression.object;
}
ChainElement::TSNonNullExpression(_) | ChainElement::PrivateFieldExpression(_) => {
break None;
}
},
_ => break None,
}
}
}
fn chain_root_ident<'a>(expr: &'a Expression) -> Option<&'a str> {
chain_root_visiting_eager_children(expr, |_| {})
}
fn for_each_eager_chain_child<'a>(expr: &'a Expression, visit: impl FnMut(EagerChild<'a>)) {
let _ = chain_root_visiting_eager_children(expr, visit);
}
impl GlobalContext<'_> for StmtEvalAnalyzer<'_> {
fn is_global_reference(&self, reference: &IdentifierReference<'_>) -> bool {
self.is_unresolved_reference(reference)
}
}
impl MayHaveSideEffectsContext<'_> for StmtEvalAnalyzer<'_> {
fn engine_targets(&self) -> Option<&EngineTargets> {
Some(&self.options.transform_options.target)
}
fn annotations(&self) -> bool {
!self.flat_options.ignore_annotations()
}
fn manual_pure_functions(&self, callee: &Expression) -> bool {
self.is_expr_manual_pure_functions(callee)
&& !self.analyze_eager_chain_children(callee).has_side_effect_for_tree_shaking()
}
fn property_read_side_effects(&self) -> PropertyReadSideEffects {
if self.flat_options.property_read_side_effects() {
PropertyReadSideEffects::All
} else {
PropertyReadSideEffects::None
}
}
fn unknown_global_side_effects(&self) -> bool {
self.options.treeshake.unknown_global_side_effects()
}
fn property_write_side_effects(&self) -> bool {
self.flat_options.property_write_side_effects()
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use itertools::Itertools;
use oxc::{parser::Parser, span::SourceType};
use rolldown_common::{
AstScopes, ExperimentalOptions, InnerOptions, NormalizedBundlerOptions,
PropertyReadSideEffects, PropertyWriteSideEffects, StmtEvalFlags,
};
use rolldown_ecmascript::{EcmaAst, EcmaCompiler};
use super::StmtEvalAnalyzer;
use rolldown_common::FlatOptions;
fn has_side_effect_for_tree_shaking(code: &str) -> bool {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let options = Arc::new(NormalizedBundlerOptions::default());
let flags = FlatOptions::from_shared_options(&options);
ast.program().body.iter().any(|stmt| {
StmtEvalAnalyzer::new(&ast_scopes, flags, &options, None, None)
.analyze_stmt(stmt)
.has_side_effect_for_tree_shaking()
})
}
fn has_side_effect_for_tree_shaking_with_target(code: &str, target: &str) -> bool {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let mut options = NormalizedBundlerOptions::default();
options.transform_options.target =
oxc::transformer::EngineTargets::from_target(target).expect("valid target");
let options = Arc::new(options);
let flags = FlatOptions::from_shared_options(&options);
ast.program().body.iter().any(|stmt| {
StmtEvalAnalyzer::new(&ast_scopes, flags, &options, None, None)
.analyze_stmt(stmt)
.has_side_effect_for_tree_shaking()
})
}
fn get_stmt_eval_flags(code: &str) -> Vec<StmtEvalFlags> {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let options = Arc::new(NormalizedBundlerOptions::default());
let flags = FlatOptions::from_shared_options(&options);
ast
.program()
.body
.iter()
.map(|stmt| {
StmtEvalAnalyzer::new(&ast_scopes, flags, &options, None, None)
.analyze_stmt(stmt)
.tree_shaking_flags()
})
.collect_vec()
}
fn get_stmt_order_sensitivity(code: &str) -> Vec<bool> {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let options = Arc::new(NormalizedBundlerOptions::default());
let flags = FlatOptions::from_shared_options(&options);
ast
.program()
.body
.iter()
.map(|stmt| {
StmtEvalAnalyzer::new(&ast_scopes, flags, &options, None, None)
.analyze_stmt(stmt)
.is_order_sensitive()
})
.collect_vec()
}
fn get_stmt_eval_with_options(
code: &str,
options: &Arc<NormalizedBundlerOptions>,
) -> Vec<(StmtEvalFlags, bool)> {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let flags = FlatOptions::from_shared_options(options);
ast
.program()
.body
.iter()
.map(|stmt| {
let facts =
StmtEvalAnalyzer::new(&ast_scopes, flags, options, None, None).analyze_stmt(stmt);
(facts.tree_shaking_flags(), facts.is_order_sensitive())
})
.collect_vec()
}
fn get_stmt_eval_with_top_level_eager_order_reasons(
code: &str,
options: &Arc<NormalizedBundlerOptions>,
) -> Vec<(StmtEvalFlags, bool)> {
let source_type = SourceType::tsx();
let ast = EcmaCompiler::parse("<Noop>", code, source_type).unwrap();
let semantic = EcmaAst::make_semantic(ast.program());
let scoping = semantic.into_scoping();
let ast_scopes = AstScopes::new(scoping);
let flags = FlatOptions::from_shared_options(options);
ast
.program()
.body
.iter()
.map(|stmt| {
let analyzer = StmtEvalAnalyzer::new(&ast_scopes, flags, options, None, None);
let mut facts = analyzer.analyze_stmt(stmt);
if options.is_strict_execution_order_enabled() && !facts.is_order_sensitive() {
analyzer.add_top_level_eager_order_reasons(stmt, &mut facts);
}
(facts.tree_shaking_flags(), facts.is_order_sensitive())
})
.collect_vec()
}
fn strict_on_demand_options(treeshake: InnerOptions) -> Arc<NormalizedBundlerOptions> {
Arc::new(NormalizedBundlerOptions {
treeshake: treeshake.into(),
experimental: ExperimentalOptions {
on_demand_wrapping: Some(true),
..ExperimentalOptions::default()
},
strict_execution_order: true,
..NormalizedBundlerOptions::default()
})
}
fn assert_eager_reason_only(
code: &str,
options: &Arc<NormalizedBundlerOptions>,
expected_flags: StmtEvalFlags,
) {
let regular = get_stmt_eval_with_options(code, options);
let eager = get_stmt_eval_with_top_level_eager_order_reasons(code, options);
assert_eq!(regular, vec![(expected_flags, false)], "regular facts for {code}");
assert_eq!(eager, vec![(expected_flags, true)], "eager facts for {code}");
}
fn assert_last_statement_gets_eager_reason_only(
code: &str,
options: &Arc<NormalizedBundlerOptions>,
expected_flags: StmtEvalFlags,
) {
let regular = get_stmt_eval_with_options(code, options);
let eager = get_stmt_eval_with_top_level_eager_order_reasons(code, options);
assert_eq!(
regular.iter().map(|(flags, _)| *flags).collect_vec(),
eager.iter().map(|(flags, _)| *flags).collect_vec(),
"tree-shaking flags for {code}"
);
assert_eq!(regular.last(), Some(&(expected_flags, false)), "regular facts for {code}");
assert_eq!(eager.last(), Some(&(expected_flags, true)), "eager facts for {code}");
assert!(
eager[..eager.len() - 1].iter().all(|(_, is_order_sensitive)| !is_order_sensitive),
"deferred declarations for {code}"
);
}
#[test]
fn test_side_effect_for_tree_shaking() {
assert!(!has_side_effect_for_tree_shaking("export { a }"));
assert!(!has_side_effect_for_tree_shaking("const a = {}"));
assert!(!has_side_effect_for_tree_shaking(
"const PatchFlags = {
'TEXT':1,
'1':'TEXT',
'CLASS':2,
'2':'CLASS',
'STYLE':4,
'4':'STYLE',
'PROPS':8,
'8':'PROPS',
'FULL_PROPS':16,
'16':'FULL_PROPS',
'NEED_HYDRATION':32,
'32':'NEED_HYDRATION',
'STABLE_FRAGMENT':64,
'64':'STABLE_FRAGMENT',
'KEYED_FRAGMENT':128,
'128':'KEYED_FRAGMENT',
'UNKEYED_FRAGMENT':256,
'256':'UNKEYED_FRAGMENT',
'NEED_PATCH':512,
'512':'NEED_PATCH',
'DYNAMIC_SLOTS':1024,
'1024':'DYNAMIC_SLOTS',
'DEV_ROOT_FRAGMENT':2048,
'2048':'DEV_ROOT_FRAGMENT',
'HOISTED': -1,
'-1':'HOISTED',
'BAIL': -2,
'-2':'BAIL'
};",
));
}
#[test]
fn test_template_literal() {
assert!(!has_side_effect_for_tree_shaking("`hello`"));
assert!(has_side_effect_for_tree_shaking("const foo = ''; `hello${foo}`"));
assert!(has_side_effect_for_tree_shaking("`hello${foo}`"));
assert!(has_side_effect_for_tree_shaking("const foo = {}; `hello${foo.bar}`"));
assert!(has_side_effect_for_tree_shaking("tag`hello`"));
}
#[test]
fn test_logical_expression() {
assert!(!has_side_effect_for_tree_shaking("true && false"));
assert!(!has_side_effect_for_tree_shaking("null ?? true"));
assert!(has_side_effect_for_tree_shaking("true && bar"));
assert!(has_side_effect_for_tree_shaking("foo ?? true"));
}
#[test]
fn test_parenthesized_expression() {
assert!(!has_side_effect_for_tree_shaking("(true)"));
assert!(!has_side_effect_for_tree_shaking("(null)"));
assert!(has_side_effect_for_tree_shaking("(bar)"));
assert!(has_side_effect_for_tree_shaking("(foo)"));
}
#[test]
fn test_sequence_expression() {
assert!(!has_side_effect_for_tree_shaking("true, false"));
assert!(!has_side_effect_for_tree_shaking("null, true"));
assert!(has_side_effect_for_tree_shaking("true, bar"));
assert!(has_side_effect_for_tree_shaking("foo, true"));
}
#[test]
fn test_conditional_expression() {
assert!(!has_side_effect_for_tree_shaking("true ? false : true"));
assert!(!has_side_effect_for_tree_shaking("null ? true : false"));
assert!(has_side_effect_for_tree_shaking("true ? bar : true"));
assert!(has_side_effect_for_tree_shaking("foo ? true : false"));
assert!(has_side_effect_for_tree_shaking("true ? bar : true"));
}
#[test]
fn test_block_statement() {
assert!(!has_side_effect_for_tree_shaking("{ }"));
assert!(!has_side_effect_for_tree_shaking("{ const a = 1; }"));
assert!(!has_side_effect_for_tree_shaking("{ const a = 1; const b = 2; }"));
assert!(has_side_effect_for_tree_shaking("{ const a = 1; bar; }"));
}
#[test]
fn test_do_while_statement() {
assert!(!has_side_effect_for_tree_shaking("do { } while (true)"));
assert!(!has_side_effect_for_tree_shaking("do { const a = 1; } while (true)"));
assert!(has_side_effect_for_tree_shaking("do { const a = 1; } while (bar)"));
assert!(has_side_effect_for_tree_shaking("do { const a = 1; bar; } while (true)"));
assert!(has_side_effect_for_tree_shaking("do { bar; } while (true)"));
}
#[test]
fn test_while_statement() {
assert!(!has_side_effect_for_tree_shaking("while (true) { }"));
assert!(!has_side_effect_for_tree_shaking("while (true) { const a = 1; }"));
assert!(has_side_effect_for_tree_shaking("while (bar) { const a = 1; }"));
assert!(has_side_effect_for_tree_shaking("while (true) { const a = 1; bar; }"));
assert!(has_side_effect_for_tree_shaking("while (true) { bar; }"));
}
#[test]
fn test_if_statement() {
assert!(!has_side_effect_for_tree_shaking("if (true) { }"));
assert!(!has_side_effect_for_tree_shaking("if (true) { const a = 1; }"));
assert!(has_side_effect_for_tree_shaking("if (bar) { const a = 1; }"));
assert!(has_side_effect_for_tree_shaking("if (true) { const a = 1; bar; }"));
assert!(has_side_effect_for_tree_shaking("if (true) { bar; }"));
}
#[test]
fn test_empty_statement() {
assert!(!has_side_effect_for_tree_shaking(";"));
assert!(!has_side_effect_for_tree_shaking(";;"));
}
#[test]
fn test_continue_statement() {
assert!(!has_side_effect_for_tree_shaking("continue;"));
}
#[test]
fn test_break_statement() {
assert!(!has_side_effect_for_tree_shaking("break;"));
}
#[test]
fn test_return_statement() {
assert!(!has_side_effect_for_tree_shaking("return;"));
assert!(!has_side_effect_for_tree_shaking("return 1;"));
assert!(has_side_effect_for_tree_shaking("return bar;"));
}
#[test]
fn test_labeled_statement() {
assert!(!has_side_effect_for_tree_shaking("label: { }"));
assert!(!has_side_effect_for_tree_shaking("label: { const a = 1; }"));
assert!(has_side_effect_for_tree_shaking("label: { const a = 1; bar; }"));
assert!(has_side_effect_for_tree_shaking("label: { bar; }"));
}
#[test]
fn test_try_statement() {
assert!(!has_side_effect_for_tree_shaking("try { } catch (e) { }"));
assert!(!has_side_effect_for_tree_shaking("try { const a = 1; } catch (e) { }"));
assert!(!has_side_effect_for_tree_shaking("try { } catch (e) { const a = 1; }"));
assert!(!has_side_effect_for_tree_shaking("try { const a = 1; } catch (e) { const a = 1; }"));
assert!(!has_side_effect_for_tree_shaking("try { const a = 1; } finally { }"));
assert!(!has_side_effect_for_tree_shaking("try { } catch (e) { const a = 1; } finally { }"));
assert!(!has_side_effect_for_tree_shaking("try { } catch (e) { } finally { const a = 1; }"));
assert!(!has_side_effect_for_tree_shaking(
"try { const a = 1; } catch (e) { const a = 1; } finally { const a = 1; }"
));
assert!(has_side_effect_for_tree_shaking("try { const a = 1; bar; } catch (e) { }"));
assert!(has_side_effect_for_tree_shaking("try { } catch (e) { const a = 1; bar; }"));
assert!(has_side_effect_for_tree_shaking("try { } catch (e) { bar; }"));
assert!(has_side_effect_for_tree_shaking("try { const a = 1; } catch (e) { bar; }"));
assert!(has_side_effect_for_tree_shaking("try { bar; } finally { }"));
assert!(has_side_effect_for_tree_shaking("try { } catch (e) { bar; } finally { }"));
assert!(has_side_effect_for_tree_shaking("try { } catch (e) { } finally { bar; }"));
assert!(has_side_effect_for_tree_shaking("try { bar; } catch (e) { bar; } finally { bar; }"));
}
#[test]
fn test_switch_statement() {
assert!(!has_side_effect_for_tree_shaking("switch (true) { }"));
assert!(!has_side_effect_for_tree_shaking("switch (true) { case 1: break; }"));
assert!(!has_side_effect_for_tree_shaking("switch (true) { case 1: break; default: break; }"));
assert!(has_side_effect_for_tree_shaking("switch (bar) { case 1: break; }"));
assert!(has_side_effect_for_tree_shaking("switch (true) { case 1: bar; }"));
assert!(has_side_effect_for_tree_shaking("switch (true) { case bar: break; }"));
assert!(has_side_effect_for_tree_shaking("switch (true) { case 1: bar; default: bar; }"));
}
#[test]
fn test_binary_expression() {
assert!(has_side_effect_for_tree_shaking("1 + foo"));
assert!(has_side_effect_for_tree_shaking("2 + bar"));
assert!(!has_side_effect_for_tree_shaking("1 + 1"));
assert!(has_side_effect_for_tree_shaking("const a = 1; const b = 2; a + b"));
}
#[test]
fn test_private_in_expression() {
assert!(has_side_effect_for_tree_shaking("#privateField in this"));
assert!(has_side_effect_for_tree_shaking("const obj = {}; #privateField in obj"));
assert!(has_side_effect_for_tree_shaking("#privateField in bar"));
assert!(has_side_effect_for_tree_shaking("#privateField in foo"));
}
#[test]
fn test_this_expression() {
assert!(has_side_effect_for_tree_shaking("this"));
assert!(has_side_effect_for_tree_shaking("this.a"));
assert!(has_side_effect_for_tree_shaking("this.a + this.b"));
assert!(has_side_effect_for_tree_shaking("this.a = 10"));
}
#[test]
fn test_meta_property_expression() {
assert!(!has_side_effect_for_tree_shaking("import.meta"));
assert!(!has_side_effect_for_tree_shaking("const meta = import.meta"));
assert!(!has_side_effect_for_tree_shaking("import.meta.url"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.url"));
assert!(!has_side_effect_for_tree_shaking("import.meta['url']"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.['url']"));
assert!(!has_side_effect_for_tree_shaking("import.meta.ROLLDOWN_FILE_URL_abc123"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.ROLLDOWN_FILE_URL_abc123"));
assert!(!has_side_effect_for_tree_shaking(
"import.meta.ROLLDOWN_FILE_URL_aaaaaaaaaaaaaaaaaaaaaa_myUrlId"
));
assert!(!has_side_effect_for_tree_shaking("import.meta['ROLLDOWN_FILE_URL_abc123']"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.['ROLLDOWN_FILE_URL_abc123']"));
assert!(!has_side_effect_for_tree_shaking("import.meta.ROLLUP_FILE_URL_abc123"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.ROLLUP_FILE_URL_abc123"));
assert!(!has_side_effect_for_tree_shaking("import.meta['ROLLUP_FILE_URL_abc123']"));
assert!(!has_side_effect_for_tree_shaking("import.meta?.['ROLLUP_FILE_URL_abc123']"));
assert!(has_side_effect_for_tree_shaking("import.meta[foo()]"));
assert!(has_side_effect_for_tree_shaking("import.meta.hot"));
assert!(has_side_effect_for_tree_shaking("import.meta['hot']"));
assert!(has_side_effect_for_tree_shaking("import.meta.nonExisting.foo"));
assert!(has_side_effect_for_tree_shaking("const { url } = import.meta"));
assert!(has_side_effect_for_tree_shaking("import.meta.url = 'test'"));
}
#[test]
fn test_assignment_expression() {
assert!(has_side_effect_for_tree_shaking("let a; [] = a"));
assert!(has_side_effect_for_tree_shaking("({} = a)"));
assert!(has_side_effect_for_tree_shaking("let a; a = 1"));
assert!(has_side_effect_for_tree_shaking("let a, b; a = b; a = b = 1"));
assert!(has_side_effect_for_tree_shaking("b = 1"));
assert!(has_side_effect_for_tree_shaking("[] = b"));
assert!(has_side_effect_for_tree_shaking("let a; a = b"));
assert!(has_side_effect_for_tree_shaking("let a; a.b = 1"));
assert!(has_side_effect_for_tree_shaking("let a; a['b'] = 1"));
assert!(has_side_effect_for_tree_shaking("let a; a = a.b"));
assert!(has_side_effect_for_tree_shaking("let a, b; ({ a } = b)"));
assert!(has_side_effect_for_tree_shaking("let a, b; ({ ...a } = b)"));
assert!(has_side_effect_for_tree_shaking("let a, b; [ a ] = b"));
assert!(has_side_effect_for_tree_shaking("let a, b; [ ...a ] = b"));
}
#[test]
fn test_chain_expression() {
assert!(!has_side_effect_for_tree_shaking("Object.create"));
assert!(!has_side_effect_for_tree_shaking("Object?.create"));
assert!(!has_side_effect_for_tree_shaking("let a; /*#__PURE__*/ a?.()"));
assert!(has_side_effect_for_tree_shaking("let a; a?.b"));
assert!(has_side_effect_for_tree_shaking("let a; a?.()"));
assert!(has_side_effect_for_tree_shaking("let a; a?.[a]"));
}
#[test]
fn test_other_statements() {
assert!(has_side_effect_for_tree_shaking("debugger;"));
assert!(has_side_effect_for_tree_shaking("for (const k in {}) { }"));
assert!(has_side_effect_for_tree_shaking("let a; for (const v of []) { a++ }"));
assert!(has_side_effect_for_tree_shaking("for (;;) { }"));
assert!(has_side_effect_for_tree_shaking("throw 1;"));
assert!(has_side_effect_for_tree_shaking("with(a) { }"));
assert!(has_side_effect_for_tree_shaking("await 1"));
assert!(has_side_effect_for_tree_shaking("import('foo')"));
assert!(has_side_effect_for_tree_shaking("let a; a``"));
assert!(has_side_effect_for_tree_shaking("let a; a++"));
}
#[test]
fn test_new_expr() {
assert!(!has_side_effect_for_tree_shaking("new Map()"));
assert!(!has_side_effect_for_tree_shaking("new Set()"));
assert!(!has_side_effect_for_tree_shaking("new Map([[1, 2], [3, 4]]);"));
assert!(has_side_effect_for_tree_shaking("new Regex()"));
assert!(!has_side_effect_for_tree_shaking(
"new Date(); new Date(''); new Date(null); new Date(false); new Date(undefined)"
));
assert!(!has_side_effect_for_tree_shaking("new Uint8Array()"));
assert!(!has_side_effect_for_tree_shaking("new Uint8Array(null)"));
assert!(!has_side_effect_for_tree_shaking("new Uint8Array(undefined)"));
assert!(!has_side_effect_for_tree_shaking("new Int8Array()"));
assert!(!has_side_effect_for_tree_shaking("new Uint16Array()"));
assert!(!has_side_effect_for_tree_shaking("new Uint32Array()"));
assert!(!has_side_effect_for_tree_shaking("new Float64Array()"));
assert!(!has_side_effect_for_tree_shaking("new BigUint64Array()"));
assert!(!has_side_effect_for_tree_shaking("new Uint8Array(10)"));
assert!(!has_side_effect_for_tree_shaking("new Int16Array(5)"));
assert!(!has_side_effect_for_tree_shaking("new Int32Array(100)"));
assert!(!has_side_effect_for_tree_shaking("new Float32Array(20)"));
assert!(!has_side_effect_for_tree_shaking("new BigInt64Array(8)"));
assert!(!has_side_effect_for_tree_shaking("new Uint8ClampedArray(256)"));
assert!(has_side_effect_for_tree_shaking("new Symbol()"));
assert!(has_side_effect_for_tree_shaking("new Symbol('string')"));
assert!(has_side_effect_for_tree_shaking("new Symbol(null)"));
assert!(has_side_effect_for_tree_shaking("new Symbol(undefined)"));
assert!(has_side_effect_for_tree_shaking("new Symbol({ toString() { throw new Error() } })"));
assert!(has_side_effect_for_tree_shaking("let unknownVariable; new Symbol(unknownVariable)"));
assert!(!has_side_effect_for_tree_shaking("Symbol()"));
assert!(!has_side_effect_for_tree_shaking("Symbol('string')"));
assert!(!has_side_effect_for_tree_shaking("Symbol(null)"));
assert!(!has_side_effect_for_tree_shaking("Symbol(undefined)"));
assert!(!has_side_effect_for_tree_shaking("Symbol(123)"));
assert!(!has_side_effect_for_tree_shaking("Symbol(true)"));
assert!(has_side_effect_for_tree_shaking("Symbol({ toString() { throw new Error() } })"));
assert!(has_side_effect_for_tree_shaking("let unknownVariable; Symbol(unknownVariable)"));
assert!(!has_side_effect_for_tree_shaking("new String()"));
assert!(!has_side_effect_for_tree_shaking("new Number()"));
assert!(!has_side_effect_for_tree_shaking("new Boolean()"));
assert!(!has_side_effect_for_tree_shaking("new Object()"));
assert!(has_side_effect_for_tree_shaking("new BigInt(123)"));
}
#[test]
fn test_primitive_global_function_calls() {
assert!(!has_side_effect_for_tree_shaking("String()"));
assert!(!has_side_effect_for_tree_shaking("String('hello')"));
assert!(!has_side_effect_for_tree_shaking("String(123)"));
assert!(!has_side_effect_for_tree_shaking("String(null)"));
assert!(!has_side_effect_for_tree_shaking("String(undefined)"));
assert!(!has_side_effect_for_tree_shaking("String(true)"));
assert!(!has_side_effect_for_tree_shaking("String({})"));
assert!(!has_side_effect_for_tree_shaking("String([1, 2, 3])"));
assert!(!has_side_effect_for_tree_shaking("let obj; String(obj)"));
assert!(!has_side_effect_for_tree_shaking("Number()"));
assert!(!has_side_effect_for_tree_shaking("Number('123')"));
assert!(!has_side_effect_for_tree_shaking("Number(456)"));
assert!(!has_side_effect_for_tree_shaking("Number(null)"));
assert!(!has_side_effect_for_tree_shaking("Number(undefined)"));
assert!(!has_side_effect_for_tree_shaking("Number(true)"));
assert!(!has_side_effect_for_tree_shaking("Number({})"));
assert!(has_side_effect_for_tree_shaking("let val; Number(val)"));
assert!(!has_side_effect_for_tree_shaking("Boolean()"));
assert!(!has_side_effect_for_tree_shaking("Boolean(true)"));
assert!(!has_side_effect_for_tree_shaking("Boolean('text')"));
assert!(!has_side_effect_for_tree_shaking("Boolean(0)"));
assert!(!has_side_effect_for_tree_shaking("Boolean(null)"));
assert!(!has_side_effect_for_tree_shaking("Boolean(undefined)"));
assert!(!has_side_effect_for_tree_shaking("Boolean({})"));
assert!(!has_side_effect_for_tree_shaking("let val; Boolean(val)"));
assert!(has_side_effect_for_tree_shaking("BigInt()"));
assert!(!has_side_effect_for_tree_shaking("BigInt(123)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(0)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(-1)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(+1)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(true)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(false)"));
assert!(!has_side_effect_for_tree_shaking("BigInt(123n)"));
assert!(!has_side_effect_for_tree_shaking("BigInt('456')"));
assert!(has_side_effect_for_tree_shaking("BigInt('abc')"));
assert!(has_side_effect_for_tree_shaking("BigInt(1.5)"));
assert!(has_side_effect_for_tree_shaking("BigInt(NaN)"));
assert!(has_side_effect_for_tree_shaking("BigInt(Infinity)"));
assert!(has_side_effect_for_tree_shaking("BigInt(-Infinity)"));
assert!(has_side_effect_for_tree_shaking("BigInt(undefined)"));
assert!(has_side_effect_for_tree_shaking("BigInt(null)"));
assert!(has_side_effect_for_tree_shaking("let val; BigInt(val)"));
assert!(has_side_effect_for_tree_shaking("BigInt({})"));
assert!(has_side_effect_for_tree_shaking("let args; BigInt(...args)"));
assert!(has_side_effect_for_tree_shaking("let args; String(...args)"));
assert!(has_side_effect_for_tree_shaking("let args; Number(...args)"));
assert!(has_side_effect_for_tree_shaking("let args; Boolean(...args)"));
}
#[test]
fn test_regexp_constructor() {
assert!(!has_side_effect_for_tree_shaking("RegExp()"));
assert!(!has_side_effect_for_tree_shaking("new RegExp()"));
assert!(!has_side_effect_for_tree_shaking("RegExp('abc')"));
assert!(!has_side_effect_for_tree_shaking("new RegExp('abc')"));
assert!(!has_side_effect_for_tree_shaking("RegExp('abc', 'g')"));
assert!(!has_side_effect_for_tree_shaking("new RegExp('abc', 'g')"));
assert!(!has_side_effect_for_tree_shaking("RegExp('abc', 'gi')"));
assert!(has_side_effect_for_tree_shaking("new RegExp('abc', 'gimsuy')"));
assert!(!has_side_effect_for_tree_shaking("RegExp(/foo/)"));
assert!(!has_side_effect_for_tree_shaking("new RegExp(/foo/)"));
assert!(has_side_effect_for_tree_shaking("RegExp('[')"));
assert!(has_side_effect_for_tree_shaking("new RegExp('[')"));
assert!(has_side_effect_for_tree_shaking("RegExp('\\\\')"));
assert!(has_side_effect_for_tree_shaking("new RegExp('\\\\')"));
assert!(has_side_effect_for_tree_shaking("RegExp('a', 'xyz')"));
assert!(has_side_effect_for_tree_shaking("new RegExp('a', 'xyz')"));
assert!(has_side_effect_for_tree_shaking("RegExp('a', 'gg')"));
assert!(has_side_effect_for_tree_shaking("new RegExp('a', 'gg')"));
assert!(has_side_effect_for_tree_shaking("let p; RegExp(p)"));
assert!(has_side_effect_for_tree_shaking("let p; new RegExp(p)"));
assert!(has_side_effect_for_tree_shaking("let f; RegExp('a', f)"));
assert!(has_side_effect_for_tree_shaking("let f; new RegExp('a', f)"));
assert!(!has_side_effect_for_tree_shaking("/abc/"));
assert!(!has_side_effect_for_tree_shaking("/abc/g"));
}
#[test]
fn test_regexp_constructor_with_engine_targets() {
assert!(has_side_effect_for_tree_shaking("new RegExp('abc', 'gimsuy')"));
assert!(!has_side_effect_for_tree_shaking_with_target("new RegExp('abc', 'gimsuy')", "es2018"));
assert!(has_side_effect_for_tree_shaking_with_target("new RegExp('abc', 'gimsuy')", "es2015"));
}
#[test]
fn test_side_effects_of_global_variable_access() {
assert!(!has_side_effect_for_tree_shaking("let a = undefined"));
assert!(!has_side_effect_for_tree_shaking("let a = void 0"));
assert!(!has_side_effect_for_tree_shaking("using undef_remove = void 0;"));
assert!(has_side_effect_for_tree_shaking("using undef_keep = void test();"));
assert!(!has_side_effect_for_tree_shaking("let a = NaN"));
assert!(!has_side_effect_for_tree_shaking("let a = String"));
assert!(!has_side_effect_for_tree_shaking("let a = Object.assign"));
assert!(!has_side_effect_for_tree_shaking("let a = Object.prototype.propertyIsEnumerable"));
assert!(!has_side_effect_for_tree_shaking("let a = Symbol.asyncDispose"));
assert!(!has_side_effect_for_tree_shaking("let a = Math.E"));
assert!(!has_side_effect_for_tree_shaking("let a = Reflect.apply"));
assert!(!has_side_effect_for_tree_shaking("let a = JSON.stringify"));
assert!(!has_side_effect_for_tree_shaking("let a = Proxy"));
assert_eq!(
get_stmt_eval_flags("let a = Proxy; let a = JSON.stringify"),
vec![StmtEvalFlags::empty(), StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("let a = Proxy; let a = JSON.stringify"),
vec![true, true]
);
assert!(has_side_effect_for_tree_shaking("let a = Object.test"));
assert!(has_side_effect_for_tree_shaking("let a = Object.prototype.two"));
assert!(has_side_effect_for_tree_shaking("let a = Reflect.something"));
assert_eq!(
get_stmt_eval_flags("let a = Reflect.something"),
vec![StmtEvalFlags::UnknownSideEffect]
);
assert_eq!(get_stmt_order_sensitivity("let a = Reflect.something"), vec![true]);
assert_eq!(
get_stmt_eval_flags("let a = /*@__PURE__ */ Reflect.something()"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("let a = /*@__PURE__ */ Reflect.something()"),
vec![true]
);
}
#[test]
fn test_object_expression() {
assert!(!has_side_effect_for_tree_shaking("const of = { [1]: 'hi'}"));
assert!(!has_side_effect_for_tree_shaking("const of = { [-1]: 'hi'}"));
assert!(!has_side_effect_for_tree_shaking("const of = { [+1]: 'hi'}"));
assert!(!has_side_effect_for_tree_shaking("let remove = { [void 0]: 'x' };"));
assert!(has_side_effect_for_tree_shaking("let keep = { [void test()]: 'x' };"));
assert!(!has_side_effect_for_tree_shaking("const of = { [{}]: 'hi'}"));
}
#[test]
fn test_pure_annotation_propagates_through_compound_expr() {
assert_eq!(
get_stmt_eval_flags("export default { foo: /* @__PURE__ */ (() => globalValue)() }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default { foo: /* @__PURE__ */ (() => globalValue)() }"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default [/* @__PURE__ */ (() => globalValue)()]"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default [/* @__PURE__ */ (() => globalValue)()]"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default (0, /* @__PURE__ */ (() => globalValue)())"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default (0, /* @__PURE__ */ (() => globalValue)())"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default true ? /* @__PURE__ */ (() => globalValue)() : null"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity(
"export default true ? /* @__PURE__ */ (() => globalValue)() : null"
),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default true && /* @__PURE__ */ (() => globalValue)()"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default true && /* @__PURE__ */ (() => globalValue)()"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default /* @__PURE__ */ (() => 2)() === 1"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default /* @__PURE__ */ (() => 2)() === 1"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default typeof /* @__PURE__ */ (() => true)()"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default typeof /* @__PURE__ */ (() => true)()"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default { [/* @__PURE__ */ (() => 'k')()]: 1 }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default { [/* @__PURE__ */ (() => 'k')()]: 1 }"),
vec![true]
);
assert_eq!(get_stmt_eval_flags("let a = { x: Proxy }"), vec![StmtEvalFlags::empty()]);
assert_eq!(get_stmt_order_sensitivity("let a = { x: Proxy }"), vec![true]);
assert_eq!(get_stmt_eval_flags("let a = [JSON.stringify]"), vec![StmtEvalFlags::empty()]);
assert_eq!(get_stmt_order_sensitivity("let a = [JSON.stringify]"), vec![true]);
assert_eq!(
get_stmt_eval_flags("export default { a: { b: [/* @__PURE__ */ (() => globalValue)()] } }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity(
"export default { a: { b: [/* @__PURE__ */ (() => globalValue)()] } }"
),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default (/* @__PURE__ */ (() => globalValue)() as string)"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity(
"export default (/* @__PURE__ */ (() => globalValue)() as string)"
),
vec![true]
);
assert_eq!(
get_stmt_eval_flags(
"export default (/* @__PURE__ */ (() => globalValue)() satisfies string)"
),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity(
"export default (/* @__PURE__ */ (() => globalValue)() satisfies string)"
),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default /* @__PURE__ */ (() => globalValue)()!"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default /* @__PURE__ */ (() => globalValue)()!"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default /* @__PURE__ */ ((() => globalValue) as any)<string>()"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity(
"export default /* @__PURE__ */ ((() => globalValue) as any)<string>()"
),
vec![true]
);
}
#[test]
fn test_pure_annotation_not_propagated_through_function_body() {
assert_eq!(
get_stmt_eval_flags("export default () => /* @__PURE__ */ pureCall()"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default () => /* @__PURE__ */ pureCall()"),
vec![false]
);
assert_eq!(
get_stmt_eval_flags("export default { foo: () => /* @__PURE__ */ pureCall() }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("export default { foo: () => /* @__PURE__ */ pureCall() }"),
vec![false]
);
}
#[test]
fn test_compound_expr_side_effectful_operand_still_unknown() {
assert!(has_side_effect_for_tree_shaking("let a = { foo: globalCall() }"));
assert!(has_side_effect_for_tree_shaking("let a = [globalCall()]"));
assert!(has_side_effect_for_tree_shaking("let a = (globalCall(), 1)"));
assert!(has_side_effect_for_tree_shaking("let a = true ? globalCall() : null"));
}
#[test]
fn test_top_level_eager_order_reasons_cover_iife_and_member_wrappers() {
let options = strict_on_demand_options(InnerOptions::default());
for code in [
"const value = (() => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5))();",
"const value = (function () { return (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); })();",
"const value = ((unused) => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5))(0);",
"const value = (() => class { static value = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); })();",
] {
assert_eager_reason_only(code, &options, StmtEvalFlags::empty());
}
let property_read_options = strict_on_demand_options(InnerOptions {
property_read_side_effects: Some(PropertyReadSideEffects::False),
..InnerOptions::default()
});
for code in [
"const value = [(/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)][0];",
"const value = ({ value: (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5) }).value;",
"const value = ({ 5: 5, 9: 9 })[(/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)];",
"const value = ({ [(/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)]: 9 })[9] ?? 5;",
"const value = (/* @__PURE__ */ make()).value;",
] {
assert_eager_reason_only(code, &property_read_options, StmtEvalFlags::empty());
}
}
#[test]
fn test_top_level_eager_order_reasons_cover_bindings_assignments_and_pure_cjs_rhs() {
let property_read_options = strict_on_demand_options(InnerOptions {
property_read_side_effects: Some(PropertyReadSideEffects::False),
..InnerOptions::default()
});
for code in [
"const { value = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5) } = {};",
"const { [(/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)]: value } = { 5: 5 };",
] {
assert_eager_reason_only(code, &property_read_options, StmtEvalFlags::empty());
}
let property_write_options = strict_on_demand_options(InnerOptions {
property_write_side_effects: Some(PropertyWriteSideEffects::False),
..InnerOptions::default()
});
assert_eager_reason_only(
"const value = (({}).value = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5));",
&property_write_options,
StmtEvalFlags::empty(),
);
let pure_cjs_options = strict_on_demand_options(InnerOptions {
unknown_global_side_effects: Some(false),
property_write_side_effects: Some(PropertyWriteSideEffects::False),
..InnerOptions::default()
});
assert_eager_reason_only(
"exports.value = [(/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)][0];",
&pure_cjs_options,
StmtEvalFlags::PureCjs,
);
}
#[test]
fn test_top_level_eager_order_reasons_cover_manual_pure_call_new_and_tagged_template() {
let make_options = strict_on_demand_options(InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
});
assert_last_statement_gets_eager_reason_only(
"function make() { return { value: (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5) }; } const value = make().value;",
&make_options,
StmtEvalFlags::empty(),
);
assert_eager_reason_only("const value = make`value`;", &make_options, StmtEvalFlags::empty());
let box_options = strict_on_demand_options(InnerOptions {
manual_pure_functions: Some(std::iter::once("Box".to_string()).collect()),
..InnerOptions::default()
});
assert_last_statement_gets_eager_reason_only(
"class Box { value = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } const value = new Box();",
&box_options,
StmtEvalFlags::empty(),
);
}
#[test]
fn test_manual_pure_chains_keep_eager_child_side_effects() {
let options = Arc::new(NormalizedBundlerOptions {
treeshake: InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
}
.into(),
..NormalizedBundlerOptions::default()
});
for code in [
"make()[effect()];",
"make(effect()).value;",
"new (make()[effect()].Box)();",
"make(effect());",
] {
assert_eq!(
get_stmt_eval_with_options(code, &options),
vec![(StmtEvalFlags::UnknownSideEffect, true)],
"{code}"
);
}
}
#[test]
fn test_manual_pure_chains_drop_side_effect_free_spreads() {
let options = Arc::new(NormalizedBundlerOptions {
treeshake: InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
}
.into(),
..NormalizedBundlerOptions::default()
});
for code in [
"make(...[]).value;",
"make(...'').value;",
"make(...`literal`).value;",
"make(...[1, 2]).value;",
] {
assert_eq!(
get_stmt_eval_with_options(code, &options),
vec![(StmtEvalFlags::empty(), false)],
"{code}"
);
}
assert_eq!(
get_stmt_eval_with_options("make(...[])();", &options),
vec![(StmtEvalFlags::empty(), true)],
"make(...[])();"
);
for code in [
"make(...[effect()]).value;",
"make(...effect()).value;",
"make(...spreadable).value;",
"make(...[...spreadable]).value;",
] {
assert_eq!(
get_stmt_eval_with_options(code, &options),
vec![(StmtEvalFlags::UnknownSideEffect, true)],
"{code}"
);
}
}
#[test]
fn test_manual_pure_chains_without_eager_children_are_side_effect_free() {
let options = Arc::new(NormalizedBundlerOptions {
treeshake: InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
}
.into(),
..NormalizedBundlerOptions::default()
});
for code in [
"function make() {} make();",
"function make() {} make.div;",
"function make() {} make.div`x`;",
"function make() {} make?.div();",
"function make() {} make()();",
"function make() {} make().div();",
] {
assert_eq!(
get_stmt_eval_with_options(code, &options).last(),
Some(&(StmtEvalFlags::empty(), false)),
"{code}"
);
}
assert_eq!(
get_stmt_eval_with_options("other();", &options).first().map(|(flags, _)| *flags),
Some(StmtEvalFlags::UnknownSideEffect),
"unlisted call must not be treated as pure"
);
}
#[test]
fn test_conditional_and_logical_iife_callees_behind_manual_pure_member() {
let options = strict_on_demand_options(InnerOptions::default());
let direct_spelling =
"const value = (true && (() => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)))();";
assert_eq!(
get_stmt_eval_with_options(direct_spelling, &options),
vec![(StmtEvalFlags::UnknownSideEffect, true)]
);
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(direct_spelling, &options),
vec![(StmtEvalFlags::UnknownSideEffect, true)]
);
let make_options = strict_on_demand_options(InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
});
for code in [
"function make() {} const value = make[(true ? (() => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)) : (() => 9))()];",
"function make() {} const value = make[(true && (() => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)))()];",
] {
let regular = get_stmt_eval_with_options(code, &make_options);
assert_eq!(regular.last(), Some(&(StmtEvalFlags::UnknownSideEffect, true)), "{code}");
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(code, &make_options),
regular,
"{code}"
);
}
}
#[test]
fn test_eager_walk_covers_constructed_classes_literal_tags_and_assignment_callees() {
let make_options = strict_on_demand_options(InnerOptions {
manual_pure_functions: Some(std::iter::once("make".to_string()).collect()),
..InnerOptions::default()
});
for code in [
"function make() {} const value = make[new class { k = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); }().k];",
"function make() {} const value = make[new class { constructor() { this.k = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } }().k];",
"function make() {} const value = make[new class { constructor(k = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5)) { this.k = k; } }().k];",
"function make() {} const value = make[new (true ? class { k = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } : class {})().k];",
"function make() {} const value = make[((s) => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5))`x`];",
"function make() {} let assigned; const value = make[(assigned = () => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5))()];",
"function make() {} let assigned; const value = make[(assigned ??= () => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5))()];",
] {
let regular = get_stmt_eval_with_options(code, &make_options);
assert_eq!(regular.last(), Some(&(StmtEvalFlags::UnknownSideEffect, true)), "{code}");
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(code, &make_options),
regular,
"{code}"
);
}
}
#[test]
fn test_extended_top_level_reasons_do_not_leak_to_the_non_strict_default() {
let manual_pure_tag = "function tag() { return (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } class C { static value = tag``; }";
let manual_pure_treeshake = || InnerOptions {
manual_pure_functions: Some(std::iter::once("tag".to_string()).collect()),
..InnerOptions::default()
};
let default_options = Arc::new(NormalizedBundlerOptions {
treeshake: manual_pure_treeshake().into(),
..NormalizedBundlerOptions::default()
});
let wrap_all_options = Arc::new(NormalizedBundlerOptions {
treeshake: manual_pure_treeshake().into(),
strict_execution_order: true,
..NormalizedBundlerOptions::default()
});
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(manual_pure_tag, &default_options),
vec![(StmtEvalFlags::empty(), false), (StmtEvalFlags::empty(), false)]
);
let strict_on_demand = strict_on_demand_options(manual_pure_treeshake());
for options in [&wrap_all_options, &strict_on_demand] {
assert_last_statement_gets_eager_reason_only(
manual_pure_tag,
options,
StmtEvalFlags::empty(),
);
}
}
#[test]
fn test_top_level_eager_order_reason_walk_keeps_deferred_boundaries() {
let options = strict_on_demand_options(InnerOptions::default());
for code in [
"const deferred = () => (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5);",
"function deferred() { return (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); }",
"class Deferred { value = (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); }",
"class Deferred { method() { return (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } }",
] {
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(code, &options),
vec![(StmtEvalFlags::empty(), false)],
"{code}"
);
}
let property_read_options = strict_on_demand_options(InnerOptions {
property_read_side_effects: Some(PropertyReadSideEffects::False),
..InnerOptions::default()
});
assert_eq!(
get_stmt_eval_with_top_level_eager_order_reasons(
"const value = ({ get value() { return (/* @__PURE__ */ globalThis.__orderRead?.() ?? 5); } }).value;",
&property_read_options,
),
vec![(StmtEvalFlags::empty(), false)],
);
}
#[test]
fn test_cjs_pattern() {
assert_eq!(
get_stmt_eval_flags("Object.defineProperty(exports, \"__esModule\", { value: true })"),
vec![StmtEvalFlags::UnknownSideEffect]
);
assert_eq!(
get_stmt_eval_flags(
r"
exports.a = function test() {};
exports['b'] = function () {
console.log('b')
};
"
),
vec![StmtEvalFlags::PureCjs, StmtEvalFlags::PureCjs]
);
assert_eq!(
get_stmt_eval_flags("exports.a = global()"),
vec![StmtEvalFlags::UnknownSideEffect | StmtEvalFlags::PureCjs]
);
assert_eq!(
get_stmt_eval_flags("exports[test()] = true"),
vec![StmtEvalFlags::UnknownSideEffect]
);
assert_eq!(
get_stmt_eval_flags(
r"
let a = {};
Object.defineProperty(a, '__esModule', { value: true });
"
),
vec![StmtEvalFlags::empty(), StmtEvalFlags::UnknownSideEffect]
);
}
#[test]
fn test_class_expr() {
assert!(!has_side_effect_for_tree_shaking(
r"
let remove14 = class {
static [undefined] = 'x';
}
let remove15 = class {
static [void 0] = 'x';
}
let remove15 = class {
[void 0] = 'x';
}
"
));
}
#[test]
fn test_class_decorators() {
assert!(has_side_effect_for_tree_shaking("function fn() {} @fn class Class {}"));
assert!(has_side_effect_for_tree_shaking("function fn() {} var MyClass = @fn class {}"));
assert!(has_side_effect_for_tree_shaking("function fn() {} class MyClass { @fn accessor x }"));
assert!(has_side_effect_for_tree_shaking(
"function fn() {} class MyClass { @fn static accessor x }"
));
assert!(has_side_effect_for_tree_shaking("function fn() {} class MyClass { @fn method() {} }"));
assert!(has_side_effect_for_tree_shaking("function fn() {} class MyClass { @fn field }"));
}
#[test]
fn test_class_definition_time_order_sensitivity() {
assert_eq!(get_stmt_order_sensitivity("class C { static x = Math.max(1, 2) }"), vec![true]);
assert_eq!(
get_stmt_eval_flags("class C { static x = Math.max(1, 2) }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(
get_stmt_order_sensitivity("class C { static x = /* @__PURE__ */ (() => globalValue)() }"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("class C { static x = /* @__PURE__ */ (() => globalValue)() }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(get_stmt_order_sensitivity("class C extends Object {}"), vec![true]);
assert_eq!(get_stmt_eval_flags("class C extends Object {}"), vec![StmtEvalFlags::empty()]);
assert_eq!(get_stmt_order_sensitivity("class C { [Math.max(1, 2)]() {} }"), vec![true]);
assert_eq!(
get_stmt_eval_flags("class C { [Math.max(1, 2)]() {} }"),
vec![StmtEvalFlags::empty()]
);
assert_eq!(get_stmt_order_sensitivity("class C { static { Math.max(1, 2) } }"), vec![true]);
assert_eq!(
get_stmt_order_sensitivity("const C = class { static x = Math.max(1, 2) }"),
vec![true]
);
assert_eq!(
get_stmt_order_sensitivity("export default class { static x = Math.max(1, 2) }"),
vec![true]
);
assert_eq!(
get_stmt_eval_flags("export default class { static x = Math.max(1, 2) }"),
vec![StmtEvalFlags::empty()]
);
}
#[test]
fn test_class_non_definition_time_stays_order_insensitive() {
assert_eq!(get_stmt_order_sensitivity("class C {}"), vec![false]);
assert_eq!(get_stmt_order_sensitivity("class C { x = Math.max(1, 2) }"), vec![false]);
assert_eq!(get_stmt_eval_flags("class C { x = Math.max(1, 2) }"), vec![StmtEvalFlags::empty()]);
assert_eq!(get_stmt_order_sensitivity("const C = class { x = Math.max(1, 2) }"), vec![false]);
assert_eq!(
get_stmt_order_sensitivity("export default class { x = Math.max(1, 2) }"),
vec![false]
);
assert_eq!(
get_stmt_order_sensitivity("class C { m() { return Math.max(1, 2) } }"),
vec![false]
);
}
#[test]
fn test_class_definition_time_reason_walk_is_independent_of_tree_shaking() {
let cjs_options = NormalizedBundlerOptions {
treeshake: InnerOptions {
unknown_global_side_effects: Some(false),
property_write_side_effects: Some(PropertyWriteSideEffects::False),
..InnerOptions::default()
}
.into(),
..NormalizedBundlerOptions::default()
};
let cjs_options = Arc::new(cjs_options);
for (code, expected_order_sensitive) in [
("export class C { static x = [exports.a = 1] }", false),
("export class C { static x = [exports.a = 1, Math.max(1, 2)] }", true),
("export class C { static x = [exports.a = 1, /* @__PURE__ */ (() => 1)()] }", true),
("export class C { static x = [Math.max(1, 2), exports.a = 1] }", true),
("export class C { static { { exports.a = 1; Math.max(1, 2) } } }", true),
("export class C { static { const x = 1; Math.max(x, 2) } }", true),
("export class C { static x = (({}).x = Math.max(1, 2)) }", true),
] {
assert_eq!(
get_stmt_eval_with_options(code, &cjs_options),
vec![(StmtEvalFlags::empty(), expected_order_sensitive)],
"{code}"
);
}
let property_read_options = NormalizedBundlerOptions {
treeshake: InnerOptions {
property_read_side_effects: Some(PropertyReadSideEffects::False),
..InnerOptions::default()
}
.into(),
..NormalizedBundlerOptions::default()
};
let property_read_options = Arc::new(property_read_options);
for (code, expected_order_sensitive) in [
("class C { static { const [x = 1] = [] } }", false),
("class C { static { const [x = Math.max(1, 2)] = [] } }", true),
("class C { static x = ({})[Math.max(1, 2)] }", true),
] {
assert_eq!(
get_stmt_eval_with_options(code, &property_read_options),
vec![(StmtEvalFlags::empty(), expected_order_sensitive)],
"{code}"
);
}
assert_eq!(
get_stmt_eval_with_options(
"class C { static x = (() => Math.max(1, 2))() }",
&Arc::new(NormalizedBundlerOptions::default()),
),
vec![(StmtEvalFlags::empty(), true)]
);
assert_eq!(
get_stmt_eval_with_options(
"class C { static x = () => Math.max(1, 2) }",
&Arc::new(NormalizedBundlerOptions::default()),
),
vec![(StmtEvalFlags::empty(), false)]
);
}
#[test]
fn test_chain_root_ident() {
assert_eq!(chain_root_ident_helper("a.b"), "a");
assert_eq!(chain_root_ident_helper("styled?.div()"), "styled");
assert_eq!(chain_root_ident_helper("styled()"), "styled");
assert_eq!(chain_root_ident_helper("styled().div"), "styled");
assert_eq!(chain_root_ident_helper("styled()()"), "styled");
}
fn chain_root_ident_helper(code: &str) -> String {
let allocator = oxc::allocator::Allocator::default();
let parser = Parser::new(&allocator, code, SourceType::ts());
let expr = parser.parse_expression().unwrap();
super::chain_root_ident(&expr).unwrap().to_string()
}
}