Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/oxc_minifier/src/peephole/normalize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down
27 changes: 27 additions & 0 deletions crates/oxc_minifier/src/peephole/remove_unused_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {}");
}
}
117 changes: 72 additions & 45 deletions crates/oxc_semantic/src/binder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
}
});
}
}
}

Expand Down Expand Up @@ -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);
}
}
}
}

Expand Down
9 changes: 9 additions & 0 deletions crates/oxc_semantic/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use oxc_syntax::{
scope::ScopeId,
symbol::{RedeclarationId, SymbolFlags, SymbolId},
};
use rustc_hash::FxHashSet;

/// Symbol Table
///
Expand All @@ -28,6 +29,9 @@ pub struct SymbolTable {

pub references: IndexVec<ReferenceId, Reference>,

/// Function or Variable Symbol IDs that are marked with `@__NO_SIDE_EFFECTS__`.
pub(crate) no_side_effects: FxHashSet<SymbolId>,

inner: SymbolTableCell,
}

Expand All @@ -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),
Expand Down Expand Up @@ -332,6 +337,10 @@ impl SymbolTable {
});
self.references.reserve(additional_references);
}

pub fn no_side_effects(&self) -> &FxHashSet<SymbolId> {
&self.no_side_effects
}
}

/// Checks whether the a identifier reference is a global value or not.
Expand Down
Loading