Skip to content

feat(es/minifier): Fine grained effect analysis of class#11814

Open
Austaras wants to merge 4 commits intoswc-project:mainfrom
Austaras:main
Open

feat(es/minifier): Fine grained effect analysis of class#11814
Austaras wants to merge 4 commits intoswc-project:mainfrom
Austaras:main

Conversation

@Austaras
Copy link
Copy Markdown
Member

Description:

BREAKING CHANGE:

Related issue (if exists):
Closes #11730

Copilot AI review requested due to automatic review settings April 21, 2026 16:00
@Austaras Austaras requested a review from a team as a code owner April 21, 2026 16:00
@@ -1,3 +0,0 @@
class c {
[c] = 42;
}
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 actually fine. Refer class itself in computed key is a TDZ violation like let a = a so it's fine to remove it. I wasn't aware of this when I introduce used_recursively.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 108f868dea

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
Comment on lines +546 to 548
let Some(side_effects) =
extract_class_side_effect(self.ctx.expr_ctx, Some(ident), class)
else {
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 👍 / 👎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes SWC minifier dead-code elimination for classes that self-reference inside their body (notably when extending an external/unknown superclass), addressing #11730.

Changes:

  • Treat class identifiers as “recursive usage” while visiting class bodies to avoid counting self-references as external usage.
  • Refine class side-effect extraction (superclass expr, static field initializers, and simple static blocks) to enable dropping more unused classes while preserving necessary effects.
  • Update/add fixtures and snapshot outputs reflecting the new DCE behavior across terser tests, tsc-reference golden files, and bundle-size snapshot.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
crates/swc_ecma_minifier/src/usage_analyzer/analyzer/mod.rs Marks class ids as recursively-used while analyzing class bodies to avoid keeping unused self-referential classes.
crates/swc_ecma_minifier/src/compress/optimize/util.rs Extends extract_class_side_effect to consider class ident, super, and static blocks during side-effect extraction.
crates/swc_ecma_minifier/src/compress/optimize/unused.rs Uses the refined class side-effect extraction when dropping unused class declarations.
crates/swc_ecma_minifier/src/compress/optimize/mod.rs Routes unused class-expression elimination through the updated side-effect extraction logic.
crates/swc_ecma_minifier/tests/fixture/issues/11730/input.js Adds a regression input for #11730.
crates/swc_ecma_minifier/tests/fixture/issues/11730/output.js Adds expected output showing the class eliminated with Unknown preserved.
crates/swc_ecma_minifier/tests/terser/compress/classes/class_recursive_refs/output.js Updates expected output reflecting more aggressive removal of unused classes.
crates/swc_ecma_minifier/tests/terser/compress/classes/class_duplication/output.js Updates expected output (export default class handling) under new class-DCE behavior.
crates/swc_ecma_minifier/tests/terser/compress/classes/class_duplication_2/output.js Updates expected output for class-in-call contexts under new class-DCE behavior.
crates/swc_ecma_minifier/tests/fixture/issues/10981/output.js Updates expected output based on revised class/static side-effect handling.
crates/swc_ecma_minifier/tests/benches-full/d3.js Updates benchmark golden output showing different class retention/extraction decisions.
crates/swc_ecma_minifier/tests/libs-size.snapshot.md Updates gzip-size snapshot impacted by new minification output.
crates/swc/tests/tsc-references/uniqueSymbols.2.minified.js Updates golden output reflecting new class/static-block DCE behavior.
crates/swc/tests/tsc-references/uniqueSymbolsDeclarations.2.minified.js Updates golden output reflecting new class/static-block DCE behavior.
crates/swc/tests/tsc-references/superSymbolIndexedAccess3.2.minified.js Updates golden output reflecting class dropping when only side effects remain.
crates/swc/tests/tsc-references/classStaticBlock15(target=esnext).2.minified.js Updates golden output for static-block related transformations.
crates/swc/tests/tsc-references/classStaticBlock15(target=es2022).2.minified.js Updates golden output for static-block related transformations.
crates/swc/tests/tsc-references/assignParameterPropertyToPropertyDeclarationESNext.2.minified.js Updates golden output reflecting removal of unused classes.
crates/swc/tests/tsc-references/assignParameterPropertyToPropertyDeclarationES2022.2.minified.js Updates golden output reflecting removal of unused classes.
Comments suppressed due to low confidence (1)

crates/swc_ecma_minifier/src/compress/optimize/util.rs:187

  • PropName::Computed keys are evaluated during class definition. We currently extract key.expr side effects without guarding against references that are only valid/meaningful inside class evaluation (e.g. super) or against self-references to the class identifier. This can produce invalid output when the class is dropped (e.g. extracting leak(Dead) from [leak(Dead)]() after removing class Dead). Consider applying the same contains_super / IdentUsageFinder checks used for static initializers (and also consider contains_this_expr to preserve strict-mode this semantics) before extracting computed key expressions (both for methods and for computed class props).
            ClassMember::Method(ClassMethod {
                key: PropName::Computed(key),
                ..
            }) => {
                if key.expr.may_have_side_effects(expr_ctx) {

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 21, 2026

⚠️ No Changeset found

Latest commit: 05e4d41

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 21, 2026

Merging this PR will not alter performance

✅ 219 untouched benchmarks
⏩ 31 skipped benchmarks1


Comparing Austaras:main (05e4d41) with main (be38316)

Open in CodSpeed

Footnotes

  1. 31 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 213fa63c1d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
Comment on lines +611 to 613
self.used_recursively
.insert(id.clone(), RecursiveUsage::FnOrClass);
n.visit_children_with(self);
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 Treat self-referential extends as live class usage

Marking class declarations as used_recursively before visiting children suppresses normal usage counting for references inside the class heritage. With this change, class C extends C {} can satisfy the usage_count == 0 drop condition, and the class may be erased even though evaluating the original heritage throws ReferenceError (TDZ). Heritage self-references should prevent dead-class removal.

Useful? React with 👍 / 👎.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 21, 2026

Binary Sizes

File Size
swc.linux-x64-gnu.node 27M (27801288 bytes)

Commit: a5ed7e1

Copilot AI review requested due to automatic review settings April 21, 2026 16:21
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ff7a50d9b3

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
Comment on lines 183 to 190
ClassMember::Method(ClassMethod {
key: PropName::Computed(key),
..
}) => {
if key.expr.may_have_side_effects(expr_ctx) {
res.push(&mut key.expr);
key_res.push(&mut key.expr);
}
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

For computed method names, key.expr can reference the class name binding (e.g. class C { [C.foo()](){} }). If this key expression is extracted while removing the class, it will run without the class binding and can change behavior (ReferenceError / different lookup). extract_class_side_effect checks IdentUsageFinder for static field values and static blocks but not for computed keys; consider applying the same ident self-reference check to key.expr before collecting it (or returning None if referenced).

Copilot uses AI. Check for mistakes.
Comment on lines 192 to 197
ClassMember::ClassProp(p) => {
if let PropName::Computed(key) = &mut p.key {
if key.expr.may_have_side_effects(expr_ctx) {
res.push(&mut key.expr);
key_res.push(&mut key.expr);
}
}
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

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

For computed class field names, key.expr can reference the class name binding. When the class is dropped and side effects are extracted, evaluating such a key expression outside the class body can become invalid. extract_class_side_effect should also guard PropName::Computed(key) with the ident self-reference check (similar to the checks done for p.value / static blocks), otherwise the minifier may emit extracted expressions that reference a now-removed class binding.

Copilot uses AI. Check for mistakes.
@Austaras
Copy link
Copy Markdown
Member Author

Well it's my first time to find out that we run babel tests with compress. Maybe just ignore those tests?

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ded02a2d2c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

}
}

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 👍 / 👎.

Copilot AI review requested due to automatic review settings April 24, 2026 06:56
@kdy1
Copy link
Copy Markdown
Member

kdy1 commented Apr 24, 2026

I added some test cases to ensure that this change does not break any real app

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 173 to 179
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);
value.push(e);
}
}
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 +205 to +211
if let Some(id) = ident {
if IdentUsageFinder::find(id, v) {
return None;
}
}

value.push(v);
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.
Comment on lines 175 to 179
if let Some(e) = &mut c.super_class {
if e.may_have_side_effects(expr_ctx) {
res.push(e);
value.push(e);
}
}
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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c1dc79f23

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs Outdated
@Austaras
Copy link
Copy Markdown
Member Author

I have deleted the test class_self_reference_guard. As I stated before we shouldn't care about preserving TDZ error.

Copilot AI review requested due to automatic review settings April 24, 2026 19:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

crates/swc_ecma_minifier/src/compress/optimize/util.rs:9

  • This file already uses rustc_hash::{FxHashMap, FxHashSet}, but the new ClassEffectVisitor uses std::collections::HashSet for private_ident. For consistency (and to avoid a potentially slower hash implementation in a hot path), consider using FxHashSet<Atom> here as well.
use std::{
    borrow::Borrow,
    collections::HashSet,
    mem::{self, take},
    ops::{Deref, DerefMut},
};

use rustc_hash::{FxHashMap, FxHashSet};
use swc_atoms::{Atom, Wtf8Atom};

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread crates/swc_ecma_minifier/tests/fixture/pr/11814_class_effect_guards/output.js Outdated
Comment thread crates/swc_ecma_minifier/src/compress/optimize/util.rs
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 75c975d90e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +262 to 266
if first.may_have_side_effects(expr_ctx) {
first.visit_with(&mut visitor);
if visitor.found {
return None;
}
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 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05e4d41f9c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +546 to 548
let Some(side_effects) =
extract_class_side_effect(self.ctx.expr_ctx, Some(ident), class)
else {
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.

Comment on lines +1181 to +1183
let left = if let Expr::PrivateName(_) = &**left {
None
} else {
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

minify: self-reference in class with external superclass not detected as dead code

3 participants