Skip to content
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -1,16 +1 @@
//// [assignParameterPropertyToPropertyDeclarationES2022.ts]
class F {
Inner = class extends F {
p2 = this.p1;
};
p1 = 0;
}
class G {
p1;
Inner = class extends G {
p2 = this.p1;
};
constructor(p1){
this.p1 = p1;
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1 @@
//// [assignParameterPropertyToPropertyDeclarationESNext.ts]
class F {
Inner = class extends F {
p2 = this.p1;
};
p1 = 0;
}
class G {
p1;
Inner = class extends G {
p2 = this.p1;
};
constructor(p1){
this.p1 = p1;
}
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
//// [classStaticBlock15.ts]
var _C__1;
console.log(_C__1);
console.log(void 0);
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
//// [classStaticBlock15.ts]
var _C__1;
console.log(_C__1);
console.log(void 0);
Original file line number Diff line number Diff line change
@@ -1,12 +1,2 @@
//// [superSymbolIndexedAccess3.ts]
var symbol = Symbol.for('myThing');
class Foo {
[symbol]() {
return 0;
}
}
class Bar extends Foo {
[symbol]() {
return super[Bar]();
}
}
Symbol.for('myThing');
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,4 @@ class C0 {
this.a = s, this.b = N.s, this.c = N.s, this.d = s, this.e = N.s, this.f = N.s;
}
}
o[s], o[N.s], o[N.s], f(s), f(N.s), f(N.s), g(s), g(N.s), g(N.s), s, N.s, N.s, 2 * Math.random() && s, 2 * Math.random() && N.s, 2 * Math.random() && N.s, s, N.s;
class C1 {
static{
N.s, N.s;
}
}
o[s], o[N.s], o[N.s], f(s), f(N.s), f(N.s), g(s), g(N.s), g(N.s), s, N.s, N.s, 2 * Math.random() && s, 2 * Math.random() && N.s, 2 * Math.random() && N.s, s, N.s, N.s, N.s;
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,4 @@ class C0 {
this.a = s, this.b = N.s, this.c = N.s, this.d = s, this.e = N.s, this.f = N.s;
}
}
o[s], o[N.s], o[N.s], f(s), f(N.s), f(N.s), g(s), g(N.s), g(N.s), s, N.s, N.s, 2 * Math.random() && s, 2 * Math.random() && N.s, 2 * Math.random() && N.s, s, N.s;
class C1 {
static{
N.s, N.s;
}
}
o[s], o[N.s], o[N.s], f(s), f(N.s), f(N.s), g(s), g(N.s), g(N.s), s, N.s, N.s, 2 * Math.random() && s, 2 * Math.random() && N.s, 2 * Math.random() && N.s, s, N.s, N.s, N.s;
35 changes: 12 additions & 23 deletions crates/swc_ecma_minifier/src/compress/optimize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use swc_ecma_ast::*;
use swc_ecma_transforms_base::rename::contains_eval;
use swc_ecma_transforms_optimization::debug_assert_valid;
use swc_ecma_utils::{
prepend_stmts, prop_name_from_ident, ExprCtx, ExprExt, ExprFactory, IdentUsageFinder, IsEmpty,
ModuleItemLike, StmtLike, Type, Value,
prepend_stmts, prop_name_from_ident, ExprCtx, ExprExt, ExprFactory, IsEmpty, ModuleItemLike,
StmtLike, Type, Value,
};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith, VisitWith};
#[cfg(feature = "debug")]
Expand Down Expand Up @@ -677,26 +677,11 @@ impl Optimizer<'_> {
}

Expr::Class(cls) => {
// Do not remove class if it's self-referencing
if let Some(id) = &cls.ident {
if IdentUsageFinder::find(id, &cls.class.body) {
return Some(cls.take().into());
}
}

if cls
.class
.body
.iter()
.any(|m| m.as_static_block().iter().any(|s| !s.body.is_empty()))
{
// there's nothing we can do about it
return Some(cls.take().into());
}

let Some(side_effects) =
extract_class_side_effect(self.ctx.expr_ctx, &mut cls.class)
else {
let Some(side_effects) = extract_class_side_effect(
self.ctx.expr_ctx,
cls.ident.as_ref(),
&mut cls.class,
) else {
Comment on lines +680 to +684
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve self-referential class expressions during DCE

ignore_return_value now always delegates class expressions to extract_class_side_effect without first guarding self-references from the class name. This makes inputs like (class C { [C] = 1 }); or (class C extends C {}); droppable, even though evaluating either expression throws a TDZ ReferenceError. In this path the class name only appears in computed keys/heritage, which are not rejected by the current guard set, so dead-code elimination can silently remove required runtime errors.

Useful? React with 👍 / 👎.

return Some(cls.take().into());
};

Expand Down Expand Up @@ -1193,7 +1178,11 @@ impl Optimizer<'_> {
}) => {
report_change!("ignore_return_value: Reducing binary ({})", *op);

let left = self.ignore_return_value(left).map(Box::new);
let left = if let Expr::PrivateName(_) = &**left {
None
} else {
Comment on lines +1181 to +1183
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep private in checks when reducing binary expressions

Special-casing Expr::PrivateName on the left side to None drops the left operand entirely, which is not semantics-preserving for private-brand checks. For #x in rhs, the original code performs the in operation (including throwing when rhs is not an object), but after this change it can collapse to just evaluating rhs. When class side effects are extracted, this turns runtime TypeError cases into non-throwing behavior.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd argue this aligns with current behaviour although it is a bug. See terser/terser#1297

self.ignore_return_value(left).map(Box::new)
};
Comment on lines +1181 to +1185
let right = self.ignore_return_value(right).map(Box::new);

let mut seq = SeqExpr {
Expand Down
29 changes: 2 additions & 27 deletions crates/swc_ecma_minifier/src/compress/optimize/unused.rs
Original file line number Diff line number Diff line change
Expand Up @@ -535,32 +535,6 @@ impl Optimizer<'_> {

match decl {
Decl::Class(ClassDecl { ident, class, .. }) => {
if ident.sym == "arguments" {
return;
}

// Fix https://github.com/swc-project/swc/issues/5588
let may_have_side_effect = class.body.iter().any(|m| match m {
ClassMember::ClassProp(ClassProp {
is_static: true,
value: Some(_),
..
})
| ClassMember::PrivateProp(PrivateProp {
is_static: true,
value: Some(_),
..
}) => true,
ClassMember::StaticBlock(StaticBlock {
body: BlockStmt { stmts, .. },
..
}) if !stmts.is_empty() => true,
_ => false,
});
if may_have_side_effect {
return;
}

// If it is not used, drop it.
if self
.data
Expand All @@ -569,7 +543,8 @@ impl Optimizer<'_> {
.map(|v| v.usage_count == 0 && v.property_mutation_count == 0)
.unwrap_or(false)
{
let Some(side_effects) = extract_class_side_effect(self.ctx.expr_ctx, class)
let Some(side_effects) =
extract_class_side_effect(self.ctx.expr_ctx, Some(ident), class)
else {
Comment on lines +546 to 548
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep self-referential computed-key classes from being dropped

After the broad self-reference guard was removed, this path now relies on extract_class_side_effect to decide whether an unused class declaration can be erased, but that helper does not treat class-name references inside computed keys as effectful. Cases like class C { [C] = 1 } (or set [C](v){}) can therefore be dropped under toplevel+unused, even though evaluating the original class throws a TDZ ReferenceError, so this changes runtime semantics.

Useful? React with 👍 / 👎.

Comment on lines +546 to 548
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve strict-mode semantics when dropping class declarations

This new drop_unused_decl path hoists class static initializer/block expressions out of the class body into a plain statement, but class bodies are always strict while the surrounding scope may be sloppy. In sloppy contexts this changes observable behavior (for example, class C { static x = (undeclared = 1) } throws ReferenceError during class evaluation, while hoisted undeclared = 1; creates/updates a global without throwing). Because this commit now applies extraction to unused class declarations with static effects, it can silently change runtime semantics.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a very rare use case. Maybe in another PR.

return;
};
Expand Down
159 changes: 149 additions & 10 deletions crates/swc_ecma_minifier/src/compress/optimize/util.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{
borrow::Borrow,
mem::take,
mem::{self, take},
ops::{Deref, DerefMut},
};

Expand All @@ -10,9 +10,11 @@ use swc_common::{util::take::Take, Mark, SyntaxContext, DUMMY_SP};
use swc_ecma_ast::*;
use swc_ecma_transforms_base::perf::{Parallel, ParallelExt};
use swc_ecma_utils::{
collect_decls, contains_this_expr, prop_name_from_ident, ExprCtx, ExprExt, Remapper,
collect_decls, prop_name_from_ident, ExprCtx, ExprExt, IdentUsageFinder, Remapper,
};
use swc_ecma_visit::{
noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith,
};
use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
use tracing::debug;

use super::{Ctx, Optimizer};
Expand Down Expand Up @@ -164,17 +166,32 @@ impl Drop for WithCtx<'_, '_> {
}
}

pub(crate) fn extract_class_side_effect(
pub(crate) fn extract_class_side_effect<'a>(
expr_ctx: ExprCtx,
c: &mut Class,
) -> Option<Vec<&mut Box<Expr>>> {
ident: Option<&'a Ident>,
c: &'a mut Class,
) -> Option<Vec<&'a mut Box<Expr>>> {
let mut res = Vec::new();
let mut value = Vec::new();
if let Some(e) = &mut c.super_class {
if e.may_have_side_effects(expr_ctx) {
res.push(e);
}
}

let mut visitor = ClassEffectVisitor {
found: false,
private_ident: FxHashSet::default(),
};

for m in &mut c.body {
if let ClassMember::PrivateProp(PrivateProp { key, .. })
| ClassMember::PrivateMethod(PrivateMethod { key, .. }) = m
{
visitor.private_ident.insert(key.name.clone());
}
}
Comment on lines 174 to 193
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract_class_side_effect collects super_class into value and then appends value after computed-key effects (res.append(&mut value)). This reorders evaluation so the extends expression would run after computed property keys, but in JS class definition evaluation the heritage (extends) must be evaluated first. Consider keeping a dedicated prefix list (heritage first), then computed keys, then static initializers/blocks, or otherwise inserting super_class effects at the front of the returned sequence to preserve spec order.

Copilot uses AI. Check for mistakes.
Comment on lines 176 to 193
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For super_class, extraction is currently gated only by may_have_side_effects. But extends <class-name> (or other self-references) can throw at class evaluation time even when may_have_side_effects returns false (e.g. extends X where X is the class binding in TDZ). Consider returning None when super_class references ident (or contains super) to prevent incorrectly dropping classes that must be preserved for runtime error behavior.

Copilot uses AI. Check for mistakes.

for m in &mut c.body {
match m {
ClassMember::Method(ClassMethod {
Expand All @@ -195,10 +212,18 @@ pub(crate) fn extract_class_side_effect(

if let Some(v) = &mut p.value {
if p.is_static && v.may_have_side_effects(expr_ctx) {
if contains_this_expr(v) {
v.visit_with(&mut visitor);
if visitor.found {
Comment thread
Austaras marked this conversation as resolved.
return None;
}
res.push(v);

if let Some(id) = ident {
if IdentUsageFinder::find(id, v) {
return None;
}
}

value.push(v);
Comment on lines +220 to +226
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new self-reference guard (IdentUsageFinder::find(id, v)) only runs for static field values. Similar self-reference checks are needed for other class-evaluation-time expressions that can throw/behave differently when the class is removed, especially the extends (heritage) expression and computed member keys (which are evaluated during class definition). Without that, cases like class X extends X {} or [X](){} can become droppable even though they throw due to TDZ / depend on the class binding.

Copilot uses AI. Check for mistakes.
}
}
}
Expand All @@ -208,20 +233,134 @@ pub(crate) fn extract_class_side_effect(
..
}) => {
if v.may_have_side_effects(expr_ctx) {
if contains_this_expr(v) {
v.visit_with(&mut visitor);
if visitor.found {
return None;
}

if let Some(id) = ident {
if IdentUsageFinder::find(id, v) {
return None;
}
}

value.push(v);
}
}
ClassMember::StaticBlock(s) => {
if s.body.stmts.len() > 1 {
return None;
}

let first = if let Some(stmt) = s.body.stmts.get_mut(0) {
&mut stmt.as_mut_expr()?.expr
} else {
continue;
};

if first.may_have_side_effects(expr_ctx) {
first.visit_with(&mut visitor);
if visitor.found {
return None;
}
Comment on lines +261 to 265
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve new.target static effects in class context

This hoisting path can move static-block expressions out of the class body after only visitor.found checks, but ClassEffectVisitor never flags new.target usage. As a result, an unused class like class C { static { sideEffect(new.target); } } can be rewritten to sideEffect(new.target), where new.target is no longer in a valid class/function context. That turns valid input into a syntax error and changes observable behavior.

Useful? React with 👍 / 👎.

res.push(v);

if let Some(id) = ident {
if IdentUsageFinder::find(id, first) {
return None;
}
}

value.push(first);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep private-name static blocks inside class scope

extract_class_side_effect now lifts single-expression static blocks into a top-level sequence, but this branch only rejects this, super, and class-name references. A static block like static { #x in sideEffect() } (or obj().#x) can therefore be extracted when the class declaration is dropped, emitting a private-name expression outside the class body, which is invalid JavaScript syntax and changes a valid program into a parse error.

Useful? React with 👍 / 👎.

}
}

_ => {}
}
}

res.append(&mut value);

Some(res)
}

struct ClassEffectVisitor {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need a new visitor? Asking because it's too bad for the binary size

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't have a private name reference visitor yet, so I write one. Maybe there's one in compact, I would check later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There isn't

found: bool,
private_ident: FxHashSet<Atom>,
}

impl Visit for ClassEffectVisitor {
noop_visit_type!();

/// Don't recurse into constructor
fn visit_constructor(&mut self, _: &Constructor) {}

/// Don't recurse into fn
fn visit_fn_decl(&mut self, _: &FnDecl) {}

/// Don't recurse into fn
fn visit_fn_expr(&mut self, _: &FnExpr) {}

/// Don't recurse into fn
fn visit_function(&mut self, _: &Function) {}

/// Don't recurse into fn
fn visit_getter_prop(&mut self, n: &GetterProp) {
n.key.visit_with(self);
}

/// Don't recurse into fn
fn visit_method_prop(&mut self, n: &MethodProp) {
n.key.visit_with(self);
n.function.visit_with(self);
}

/// Don't recurse into fn
fn visit_setter_prop(&mut self, n: &SetterProp) {
n.key.visit_with(self);
n.param.visit_with(self);
Comment thread
Austaras marked this conversation as resolved.
}

fn visit_this_expr(&mut self, _: &ThisExpr) {
self.found = true;
}

fn visit_prop(&mut self, n: &Prop) {
n.visit_children_with(self);

if let Prop::Shorthand(Ident { sym, .. }) = n {
if &**sym == "arguments" {
self.found = true;
}
}
}

fn visit_super(&mut self, _: &Super) {
self.found = true;
}

fn visit_private_name(&mut self, n: &PrivateName) {
if self.private_ident.contains(&n.name) {
self.found = true
}
}

fn visit_class(&mut self, n: &Class) {
let mut new_set = FxHashSet::default();

for m in &n.body {
if let ClassMember::PrivateProp(PrivateProp { key, .. })
| ClassMember::PrivateMethod(PrivateMethod { key, .. }) = m
{
new_set.insert(key.name.clone());
}
}

let old_set = mem::replace(&mut self.private_ident, new_set);
n.visit_children_with(self);
self.private_ident = old_set;
}
}

pub(crate) fn is_valid_for_lhs(e: &Expr) -> bool {
!matches!(e, Expr::Lit(..) | Expr::Unary(..))
}
Expand Down
Loading
Loading