diff --git a/crates/oxc_minifier/src/peephole/normalize.rs b/crates/oxc_minifier/src/peephole/normalize.rs index 0adc3b702ffcb..8126a5a49132a 100644 --- a/crates/oxc_minifier/src/peephole/normalize.rs +++ b/crates/oxc_minifier/src/peephole/normalize.rs @@ -92,6 +92,14 @@ impl<'a> Traverse<'a> for Normalize { *expr = e; } } + + fn exit_call_expression(&mut self, e: &mut CallExpression<'a>, ctx: &mut TraverseCtx<'a>) { + Self::set_no_side_effects(&mut e.pure, &e.callee, ctx); + } + + fn exit_new_expression(&mut self, e: &mut NewExpression<'a>, ctx: &mut TraverseCtx<'a>) { + Self::set_no_side_effects(&mut e.pure, &e.callee, ctx); + } } impl<'a> Normalize { @@ -206,6 +214,20 @@ impl<'a> Normalize { } e.argument = ctx.ast.expression_numeric_literal(ident.span, 0.0, None, NumberBase::Decimal); } + + fn set_no_side_effects(pure: &mut bool, callee: &Expression<'a>, ctx: &mut TraverseCtx<'a>) { + if !*pure { + if let Some(ident) = callee.get_identifier_reference() { + if let Some(symbol_id) = + ctx.symbols().get_reference(ident.reference_id()).symbol_id() + { + if ctx.symbols().no_side_effects().contains(&symbol_id) { + *pure = true; + } + } + } + } + } } #[cfg(test)] diff --git a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs index 62a5da05978f1..378d080fcaf4d 100644 --- a/crates/oxc_minifier/src/peephole/remove_unused_expression.rs +++ b/crates/oxc_minifier/src/peephole/remove_unused_expression.rs @@ -901,4 +901,31 @@ mod test { test("/* @__PURE__ */ (() => x)()", ""); test("/* @__PURE__ */ (() => x)(y, z)", "y, z;"); } + + #[test] + fn no_side_effects() { + fn check(source_text: &str) { + let input = format!("{source_text}; f()"); + test(&input, source_text); + + let input = format!("{source_text}; new f()"); + test(&input, source_text); + + // TODO https://github.com/evanw/esbuild/issues/3511 + // let input = format!("{source_text}; html``"); + // test(&input, source_text); + } + check("/* @__NO_SIDE_EFFECTS__ */ function f() {}"); + check("/* @__NO_SIDE_EFFECTS__ */ export function f() {}"); + check("/* @__NO_SIDE_EFFECTS__ */ export default function f() {}"); + check("export default /* @__NO_SIDE_EFFECTS__ */ function f() {}"); + check("const f = /* @__NO_SIDE_EFFECTS__ */ function() {}"); + check("export const f = /* @__NO_SIDE_EFFECTS__ */ function() {}"); + check("/* @__NO_SIDE_EFFECTS__ */ const f = function() {}"); + check("/* @__NO_SIDE_EFFECTS__ */ export const f = function() {}"); + check("const f = /* @__NO_SIDE_EFFECTS__ */ () => {}"); + check("export const f = /* @__NO_SIDE_EFFECTS__ */ () => {}"); + check("/* @__NO_SIDE_EFFECTS__ */ const f = () => {}"); + check("/* @__NO_SIDE_EFFECTS__ */ export const f = () => {}"); + } } diff --git a/crates/oxc_semantic/src/binder.rs b/crates/oxc_semantic/src/binder.rs index 29630ed603b89..2d3359d686fec 100644 --- a/crates/oxc_semantic/src/binder.rs +++ b/crates/oxc_semantic/src/binder.rs @@ -39,58 +39,78 @@ impl<'a> Binder<'a> for VariableDeclarator<'a> { let symbol_id = builder.declare_symbol(ident.span, &ident.name, includes, excludes); ident.symbol_id.set(Some(symbol_id)); }); - return; - } - - // ------------------ var hosting ------------------ - let mut target_scope_id = builder.current_scope_id; - let mut var_scope_ids = vec![]; - - // Collect all scopes where variable hoisting can occur - for scope_id in builder.scope.ancestors(target_scope_id) { - let flags = builder.scope.get_flags(scope_id); - if flags.is_var() { - target_scope_id = scope_id; - break; - } - var_scope_ids.push(scope_id); - } - - self.id.bound_names(&mut |ident| { - let span = ident.span; - let name = ident.name; - let mut declared_symbol_id = None; - - for &scope_id in &var_scope_ids { - if let Some(symbol_id) = - builder.check_redeclaration(scope_id, span, &name, excludes, true) - { - builder.add_redeclare_variable(symbol_id, span); - declared_symbol_id = Some(symbol_id); - - // remove current scope binding and add to target scope - // avoid same symbols appear in multi-scopes - builder.scope.remove_binding(scope_id, &name); - builder.scope.add_binding(target_scope_id, &name, symbol_id); - builder.symbols.scope_ids[symbol_id] = target_scope_id; + } else { + // ------------------ var hosting ------------------ + let mut target_scope_id = builder.current_scope_id; + let mut var_scope_ids = vec![]; + + // Collect all scopes where variable hoisting can occur + for scope_id in builder.scope.ancestors(target_scope_id) { + let flags = builder.scope.get_flags(scope_id); + if flags.is_var() { + target_scope_id = scope_id; break; } + var_scope_ids.push(scope_id); } - // If a variable is already declared in the hoisted scopes, - // we don't need to create another symbol with the same name - // to make sure they point to the same symbol. - let symbol_id = declared_symbol_id.unwrap_or_else(|| { - builder.declare_symbol_on_scope(span, &name, target_scope_id, includes, excludes) + self.id.bound_names(&mut |ident| { + let span = ident.span; + let name = ident.name; + let mut declared_symbol_id = None; + + for &scope_id in &var_scope_ids { + if let Some(symbol_id) = + builder.check_redeclaration(scope_id, span, &name, excludes, true) + { + builder.add_redeclare_variable(symbol_id, span); + declared_symbol_id = Some(symbol_id); + + // remove current scope binding and add to target scope + // avoid same symbols appear in multi-scopes + builder.scope.remove_binding(scope_id, &name); + builder.scope.add_binding(target_scope_id, &name, symbol_id); + builder.symbols.scope_ids[symbol_id] = target_scope_id; + break; + } + } + + // If a variable is already declared in the hoisted scopes, + // we don't need to create another symbol with the same name + // to make sure they point to the same symbol. + let symbol_id = declared_symbol_id.unwrap_or_else(|| { + builder.declare_symbol_on_scope( + span, + &name, + target_scope_id, + includes, + excludes, + ) + }); + ident.symbol_id.set(Some(symbol_id)); + + // Finally, add the variable to all hoisted scopes + // to support redeclaration checks when declaring variables with the same name later. + for &scope_id in &var_scope_ids { + builder.hoisting_variables.entry(scope_id).or_default().insert(name, symbol_id); + } }); - ident.symbol_id.set(Some(symbol_id)); + } - // Finally, add the variable to all hoisted scopes - // to support redeclaration checks when declaring variables with the same name later. - for &scope_id in &var_scope_ids { - builder.hoisting_variables.entry(scope_id).or_default().insert(name, symbol_id); + // Save `@__NO_SIDE_EFFECTS__` for function initializers. + if let BindingPatternKind::BindingIdentifier(id) = &self.id.kind { + if let Some(symbol_id) = id.symbol_id.get() { + if let Some(init) = &self.init { + if match init { + Expression::FunctionExpression(func) => func.pure, + Expression::ArrowFunctionExpression(func) => func.pure, + _ => false, + } { + builder.symbols.no_side_effects.insert(symbol_id); + } + } } - }); + } } } @@ -202,6 +222,13 @@ impl<'a> Binder<'a> for Function<'a> { PropertyKind::Init => {} }; } + + // Save `@__NO_SIDE_EFFECTS__` + if self.pure { + if let Some(symbold_id) = self.id.as_ref().and_then(|id| id.symbol_id.get()) { + builder.symbols.no_side_effects.insert(symbold_id); + } + } } } diff --git a/crates/oxc_semantic/src/symbol.rs b/crates/oxc_semantic/src/symbol.rs index 1b9ff752b6cd5..e46f0c175487d 100644 --- a/crates/oxc_semantic/src/symbol.rs +++ b/crates/oxc_semantic/src/symbol.rs @@ -10,6 +10,7 @@ use oxc_syntax::{ scope::ScopeId, symbol::{RedeclarationId, SymbolFlags, SymbolId}, }; +use rustc_hash::FxHashSet; /// Symbol Table /// @@ -28,6 +29,9 @@ pub struct SymbolTable { pub references: IndexVec, + /// Function or Variable Symbol IDs that are marked with `@__NO_SIDE_EFFECTS__`. + pub(crate) no_side_effects: FxHashSet, + inner: SymbolTableCell, } @@ -47,6 +51,7 @@ impl Default for SymbolTable { declarations: IndexVec::new(), redeclarations: IndexVec::new(), references: IndexVec::new(), + no_side_effects: FxHashSet::default(), inner: SymbolTableCell::new(allocator, |allocator| SymbolTableInner { names: ArenaVec::new_in(allocator), resolved_references: ArenaVec::new_in(allocator), @@ -332,6 +337,10 @@ impl SymbolTable { }); self.references.reserve(additional_references); } + + pub fn no_side_effects(&self) -> &FxHashSet { + &self.no_side_effects + } } /// Checks whether the a identifier reference is a global value or not.