Latest Results
fix(es/minifier): Eliminate unused classes with cyclic references (#11963)
## Description
The minifier keeps classes that are only reachable through a cyclic
reference to each other, even when the whole cluster is unused; `terser`
removes them. Reproduced in #11934.
The bug is in the DCE tree-shaker
(`swc_ecma_transforms_optimization::simplify::dce`), which the minifier
runs through `perform_dce`.
### Root cause
`Data::subtract_cycles` cancels the internal edges of a
strongly-connected component — making its members eliminable — only when
no node in the component is a top-level entry and none has an incoming
edge from outside it.
In `Analyzer::visit_class_decl` the `extends` clause was visited
*before* the class identifier was pushed onto the dependency path
(`with_ast_path`). A reference seen with an empty path is recorded as a
top-level entry, so for `class B extends A {}` the superclass `A` was
pinned as an entry. That kept the otherwise-unreachable `A ⇄ B` cycle
(formed by the method bodies) from being subtracted, so both classes
survived.
### Fix
`visit_class_decl` now attributes the `extends` edge to the subclass
(inside the path) only when the superclass is a **backward reference to
an already-initialized class**, tracked with a small per-scope
`FxHashSet<Id>`. Forward references, non-identifier superclasses, and
everything else keep the original behavior (visited outside the path,
i.e. pinned as an entry). This preserves TDZ-observable cycles such as
`class A extends B {} class B extends A {}` — which throws at definition
time and must not be eliminated (there only `B → A` is a backward
reference, so `B` stays pinned and both classes are kept).
It also closes a latent hole in the same mechanism: a clean class in a
cycle that is referenced by a kept, side-effectful sibling (e.g. a
`static {}` block) could be dropped, leaving a dangling reference. A
class whose definition has side effects is now marked as an entry,
reusing the existing class-drop predicate (factored out as
`class_def_is_side_effect_free`), so those cycles are preserved.
### Performance
The change is confined to `dce/mod.rs` and is intentionally minimal: it
does **not** touch the `VarInfo` edge-weight struct or the
`subtract_cycles` hot loop. The only added work on the common path is
one `FxHashSet<Id>` insert per class declaration. CodSpeed should be
neutral. (An earlier, broader attempt — #11933 — grew the edge weight
and added per-declarator bookkeeping, which regressed CodSpeed; this PR
is scoped to the class-`extends` case to avoid that.)
### Tests
In `tests/simplify_dce.rs`:
- `class_extends_cycle_unused` — the repro; both classes are eliminated.
Confirmed to **fail** without the fix (`git stash` on `dce/mod.rs`).
- `class_extends_cycle_tdz_preserved` — `class A extends B {} class B
extends A {}` is kept.
- `class_extends_cycle_with_side_effect_preserved` — a cycle whose
member has a `static {}` block is kept.
`cargo test -p swc_ecma_transforms_optimization` and `cargo test -p
swc_ecma_minifier` pass with no snapshot changes (including the terser
execution tests).
## Related issue
Closes #11934
Co-authored-by: Donny/강동윤 <kdy.1997.dev@gmail.com> Latest Branches
0%
0%
baltasarblanco:fix/11934-dce-class-extends-cycle 0%
imjordanxd:feat/react-compiler-lint-diagnostics © 2026 CodSpeed Technology