fix(minifier): keep variable declaration initilized with class when keepNames is enabled (#25584)

Keep the variable declaration when that is initialized with class and keepNames is enabled:
```js
var MyStore = class { static { console.log(this.name) } };
```

fixes https://github.com/oxc-project/oxc/issues/25060

I used AI to generate the code and then reviewed the code and added the tests for `keepNames` disabled cases.
This commit is contained in:
sapphi-red committed 2026-08-14 01:23:31 +00:00
1 parent f49229d566
commit bb5a232424
3 files changed
+58 -5

No files matched your search

@@ -1,4 +1,4 @@
use super::PeepholeOptimizations;
use super::{PeepholeOptimizations, remove_unused_expression::ClassRemovability};
use crate::{CompressOptionsUnused, TraverseCtx};
use oxc_ast::ast::*;
use oxc_ecmascript::constant_evaluation::{DetermineValueType, ValueType};
@@ -64,6 +64,25 @@ impl<'a> PeepholeOptimizations {
})
}
/// Keep an unused declarator when dropping its class initializer would
/// leave an executing anonymous class expression without its inferred
/// name. The name can be observed by static blocks and decorators before
/// any later use of the binding, so `keep_names` cannot be applied after
/// the declarator has been reduced to a bare class expression.
fn class_initializer_name_needs_to_be_kept(
decl: &VariableDeclarator<'a>,
ctx: &TraverseCtx<'a>,
) -> bool {
let Some(init) = &decl.init else { return false };
if !ctx.is_expression_whose_name_needs_to_be_kept(init) {
return false;
}
let Expression::ClassExpression(class) = init.without_parentheses() else {
return false;
};
matches!(Self::classify_class_removability(class, ctx), ClassRemovability::Keep)
}
fn is_sync_iterator_expr(expr: &Expression<'a>, ctx: &TraverseCtx<'a>) -> bool {
match expr {
Expression::ArrayExpression(_)
@@ -99,10 +118,9 @@ impl<'a> PeepholeOptimizations {
match &decl.id {
BindingPattern::BindingIdentifier(ident) => {
if let Some(symbol_id) = ident.symbol_id.get() {
return Self::symbol_is_unused_by_count(symbol_id, ctx)
|| Self::self_recursive_function_declarator_is_unused(
decl, symbol_id, ctx,
);
let is_unused = Self::symbol_is_unused_by_count(symbol_id, ctx)
|| Self::self_recursive_function_declarator_is_unused(decl, symbol_id, ctx);
return is_unused && !Self::class_initializer_name_needs_to_be_kept(decl, ctx);
}
false
}
@@ -992,6 +992,20 @@ fn dce_remove_unused_class_identifier_heritage_under_assumptions() {
);
}
#[test]
fn dce_keeps_inferred_class_name_for_executing_class() {
// The static block observes the name assigned by `var MyStore = class {}`.
// DCE must not turn this into a bare anonymous class expression.
test_same("var MyStore = class { static { console.log(this.name) } };");
test_same("var MyStore = (class { static { console.log(this.name) } });");
test(
"var MyStore = (0, class { static { console.log(this.name) } });",
// this can be compressed to `class { static { console.log(this.name) } }`
"var MyStore = class { static { console.log(this.name) } };",
);
test_same("var MyStore = class { static foo = console.log(this.name) };");
}
#[test]
#[ignore = "TODO: extend recursive reachability to mutual declarators and classes"]
fn dce_recursive_unused_mutual_declarators_and_classes() {
@@ -142,6 +142,27 @@ fn remove_unused_variable_declaration() {
test_options("for (var x; ; );", "for (; ;);", &options);
test_options("for (var x = 1; ; );", "for (; ;);", &options);
test_same_options("for (var x = foo; ; );", &options); // can be improved
test_options(
"var MyStore = class { static { console.log(this.name) } };",
"(class { static { console.log(this.name) } });",
&options,
);
test_options(
"var MyStore = (class { static { console.log(this.name) } });",
"(class { static { console.log(this.name) } });",
&options,
);
test_options(
"var MyStore = (0, class { static { console.log(this.name) } });",
"(class { static { console.log(this.name) } });",
&options,
);
test_options(
"var MyStore = class { static foo = console.log(this.name) };",
"(class { static foo = console.log(this.name) });",
&options,
);
}
#[test]