Skip to content

Commit 0a96f97

Browse files
oxc-botBoshen
authored andcommitted
feat(minifier): remove unused assignment expression
closes #11469
1 parent ccf1fb4 commit 0a96f97

File tree

18 files changed

+414
-192
lines changed

18 files changed

+414
-192
lines changed

crates/oxc_ast/src/ast_impl/js.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -974,6 +974,21 @@ impl<'a> AssignmentTargetMaybeDefault<'a> {
974974
_ => None,
975975
}
976976
}
977+
978+
/// Returns mut identifier bound by this assignment target.
979+
pub fn identifier_mut(&mut self) -> Option<&mut IdentifierReference<'a>> {
980+
match self {
981+
AssignmentTargetMaybeDefault::AssignmentTargetIdentifier(id) => Some(id),
982+
Self::AssignmentTargetWithDefault(target) => {
983+
if let AssignmentTarget::AssignmentTargetIdentifier(id) = &mut target.binding {
984+
Some(id)
985+
} else {
986+
None
987+
}
988+
}
989+
_ => None,
990+
}
991+
}
977992
}
978993

979994
impl Statement<'_> {

crates/oxc_ecmascript/src/constant_evaluation/mod.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,20 @@ pub trait ConstantEvaluation<'a>: MayHaveSideEffects<'a> {
9999
}
100100
}
101101

102+
impl<'a, T: ConstantEvaluation<'a>> ConstantEvaluation<'a> for Option<T> {
103+
fn evaluate_value(&self, ctx: &impl ConstantEvaluationCtx<'a>) -> Option<ConstantValue<'a>> {
104+
self.as_ref().and_then(|t| t.evaluate_value(ctx))
105+
}
106+
107+
fn evaluate_value_to(
108+
&self,
109+
ctx: &impl ConstantEvaluationCtx<'a>,
110+
target_ty: Option<ValueType>,
111+
) -> Option<ConstantValue<'a>> {
112+
self.as_ref().and_then(|t| t.evaluate_value_to(ctx, target_ty))
113+
}
114+
}
115+
102116
impl<'a> ConstantEvaluation<'a> for IdentifierReference<'a> {
103117
fn evaluate_value_to(
104118
&self,

crates/oxc_ecmascript/src/side_effects/may_have_side_effects.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ pub trait MayHaveSideEffects<'a> {
2222
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool;
2323
}
2424

25+
impl<'a, T: MayHaveSideEffects<'a>> MayHaveSideEffects<'a> for Option<T> {
26+
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
27+
self.as_ref().is_some_and(|t| t.may_have_side_effects(ctx))
28+
}
29+
}
30+
2531
impl<'a> MayHaveSideEffects<'a> for Expression<'a> {
2632
fn may_have_side_effects(&self, ctx: &impl MayHaveSideEffectsContext<'a>) -> bool {
2733
match self {

crates/oxc_minifier/src/ctx.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use oxc_ecmascript::{
77
},
88
side_effects::{MayHaveSideEffects, PropertyReadSideEffects},
99
};
10-
use oxc_semantic::{IsGlobalReference, Scoping};
10+
use oxc_semantic::{IsGlobalReference, Scoping, SymbolId};
1111
use oxc_span::format_atom;
1212
use oxc_syntax::reference::ReferenceId;
1313

14-
use crate::{options::CompressOptions, state::MinifierState};
14+
use crate::{options::CompressOptions, state::MinifierState, symbol_value::SymbolValue};
1515

1616
pub type TraverseCtx<'a> = oxc_traverse::TraverseCtx<'a, MinifierState<'a>>;
1717

@@ -50,7 +50,7 @@ impl<'a> oxc_ecmascript::is_global_reference::IsGlobalReference<'a> for Ctx<'a,
5050
self.scoping()
5151
.get_reference(reference_id)
5252
.symbol_id()
53-
.and_then(|symbol_id| self.state.constant_values.get(&symbol_id))
53+
.and_then(|symbol_id| self.state.symbol_values.get_constant_value(symbol_id))
5454
.cloned()
5555
}
5656
}
@@ -164,6 +164,46 @@ impl<'a> Ctx<'a, '_> {
164164
false
165165
}
166166

167+
pub fn init_value(&mut self, symbol_id: SymbolId, constant: Option<ConstantValue<'a>>) {
168+
let mut exported = false;
169+
if self.scoping.current_scope_id() == self.scoping().root_scope_id() {
170+
for ancestor in self.ancestors() {
171+
if ancestor.is_export_named_declaration()
172+
|| ancestor.is_export_all_declaration()
173+
|| ancestor.is_export_default_declaration()
174+
{
175+
exported = true;
176+
}
177+
}
178+
}
179+
180+
let for_statement_init = self.ancestors().nth(1).is_some_and(|ancestor| {
181+
ancestor.is_parent_of_for_statement_init() || ancestor.is_parent_of_for_statement_left()
182+
});
183+
184+
let mut read_references_count = 0;
185+
let mut write_references_count = 0;
186+
for r in self.scoping().get_resolved_references(symbol_id) {
187+
if r.is_read() {
188+
read_references_count += 1;
189+
}
190+
if r.is_write() {
191+
write_references_count += 1;
192+
}
193+
}
194+
195+
let scope_id = self.scoping.current_scope_id();
196+
let symbol_value = SymbolValue {
197+
constant,
198+
exported,
199+
for_statement_init,
200+
read_references_count,
201+
write_references_count,
202+
scope_id,
203+
};
204+
self.state.symbol_values.init_value(symbol_id, symbol_value);
205+
}
206+
167207
/// If two expressions are equal.
168208
/// Special case `undefined` == `void 0`
169209
pub fn expr_eq(&self, a: &Expression<'a>, b: &Expression<'a>) -> bool {

crates/oxc_minifier/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ mod keep_var;
88
mod options;
99
mod peephole;
1010
mod state;
11+
mod symbol_value;
1112

1213
#[cfg(test)]
1314
mod tester;

crates/oxc_minifier/src/peephole/fold_constants.rs

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ use oxc_ecmascript::{
77
};
88
use oxc_span::GetSpan;
99
use oxc_syntax::operator::{BinaryOperator, LogicalOperator};
10-
use oxc_traverse::Ancestor;
1110

1211
use crate::ctx::Ctx;
1312

@@ -40,20 +39,6 @@ impl<'a> PeepholeOptimizations {
4039
*expr = folded_expr;
4140
ctx.state.changed = true;
4241
}
43-
44-
// Save `const value = false` into constant values.
45-
if let Ancestor::VariableDeclaratorInit(decl) = ctx.parent() {
46-
// TODO: Check for no write references.
47-
if decl.kind().is_const() {
48-
if let BindingPatternKind::BindingIdentifier(ident) = &decl.id().kind {
49-
// TODO: refactor all the above code to return value instead of expression, to avoid calling `evaluate_value` again.
50-
if let Some(value) = expr.evaluate_value(ctx) {
51-
let symbol_id = ident.symbol_id();
52-
ctx.state.constant_values.insert(symbol_id, value);
53-
}
54-
}
55-
}
56-
}
5742
}
5843

5944
#[expect(clippy::float_cmp)]
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use oxc_ast::ast::*;
2+
use oxc_ecmascript::constant_evaluation::{ConstantEvaluation, ConstantValue};
3+
use oxc_span::GetSpan;
4+
5+
use crate::ctx::Ctx;
6+
7+
use super::PeepholeOptimizations;
8+
9+
impl<'a> PeepholeOptimizations {
10+
pub fn init_symbol_value(&self, decl: &VariableDeclarator<'a>, ctx: &mut Ctx<'a, '_>) {
11+
let BindingPatternKind::BindingIdentifier(ident) = &decl.id.kind else { return };
12+
let Some(symbol_id) = ident.symbol_id.get() else { return };
13+
// Skip for `var` declarations, due to TDZ problems.
14+
if decl.kind.is_var() {
15+
return;
16+
}
17+
let value =
18+
decl.init.as_ref().map_or(Some(ConstantValue::Undefined), |e| e.evaluate_value(ctx));
19+
ctx.init_value(symbol_id, value);
20+
}
21+
22+
pub fn inline_identifier_reference(&self, expr: &mut Expression<'a>, ctx: &mut Ctx<'a, '_>) {
23+
let Expression::Identifier(ident) = expr else { return };
24+
let Some(reference_id) = ident.reference_id.get() else { return };
25+
let Some(symbol_id) = ctx.scoping().get_reference(reference_id).symbol_id() else { return };
26+
let Some(symbol_value) = ctx.state.symbol_values.get_symbol_value(symbol_id) else {
27+
return;
28+
};
29+
// Only inline single reference (for now).
30+
if symbol_value.read_references_count > 1 {
31+
return;
32+
}
33+
// Skip if there are write references.
34+
if symbol_value.write_references_count > 0 {
35+
return;
36+
}
37+
if symbol_value.for_statement_init {
38+
return;
39+
}
40+
let Some(cv) = &symbol_value.constant else { return };
41+
*expr = ctx.value_to_expr(expr.span(), cv.clone());
42+
ctx.state.changed = true;
43+
}
44+
}
45+
46+
#[cfg(test)]
47+
mod test {
48+
use crate::{
49+
CompressOptions,
50+
tester::{test_options, test_same},
51+
};
52+
53+
#[test]
54+
fn r#const() {
55+
let options = CompressOptions::smallest();
56+
test_options("const foo = 1; log(foo)", "log(1)", &options);
57+
test_options("export const foo = 1; log(foo)", "export const foo = 1; log(1)", &options);
58+
test_same("const foo = 1; log(foo), log(foo)");
59+
}
60+
}

crates/oxc_minifier/src/peephole/minimize_conditions.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,9 @@ mod test {
526526
// In the following test case, we can't remove the duplicate "alert(x);" lines since each "x"
527527
// refers to a different variable.
528528
// We only try removing duplicate statements if the AST is normalized and names are unique.
529-
test_same(
529+
test(
530530
"if (Math.random() < 0.5) { let x = 3; alert(x); } else { let x = 5; alert(x); }",
531+
"if (Math.random() < 0.5) { let x = 3; alert(3); } else { let x = 5; alert(5); }",
531532
);
532533
}
533534

crates/oxc_minifier/src/peephole/mod.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
mod convert_to_dotted_properties;
44
mod fold_constants;
5+
mod inline;
56
mod minimize_conditional_expression;
67
mod minimize_conditions;
78
mod minimize_expression_in_boolean_context;
@@ -102,10 +103,12 @@ impl<'a> PeepholeOptimizations {
102103

103104
impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
104105
fn enter_program(&mut self, _program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
106+
ctx.state.symbol_values.clear();
105107
ctx.state.changed = false;
106108
}
107109

108110
fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
111+
// Remove unused references by visiting the AST again and diff the collected references.
109112
let refs_before =
110113
ctx.scoping().resolved_references().flatten().copied().collect::<FxHashSet<_>>();
111114
let mut counter = ReferencesCounter::default();
@@ -155,18 +158,26 @@ impl<'a> Traverse<'a, MinifierState<'a>> for PeepholeOptimizations {
155158
ctx: &mut TraverseCtx<'a>,
156159
) {
157160
let mut ctx = Ctx::new(ctx);
158-
159161
self.substitute_variable_declaration(decl, &mut ctx);
160162
}
161163

162-
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
164+
fn exit_variable_declarator(
165+
&mut self,
166+
decl: &mut VariableDeclarator<'a>,
167+
ctx: &mut TraverseCtx<'a>,
168+
) {
163169
let mut ctx = Ctx::new(ctx);
170+
self.init_symbol_value(decl, &mut ctx);
171+
}
164172

173+
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
174+
let mut ctx = Ctx::new(ctx);
165175
self.fold_constants_exit_expression(expr, &mut ctx);
166176
self.minimize_conditions_exit_expression(expr, &mut ctx);
167177
self.remove_dead_code_exit_expression(expr, &mut ctx);
168178
self.replace_known_methods_exit_expression(expr, &mut ctx);
169179
self.substitute_exit_expression(expr, &mut ctx);
180+
self.inline_identifier_reference(expr, &mut ctx);
170181
}
171182

172183
fn exit_unary_expression(&mut self, expr: &mut UnaryExpression<'a>, ctx: &mut TraverseCtx<'a>) {

crates/oxc_minifier/src/peephole/normalize.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,8 @@ mod test {
454454
#[test]
455455
fn fold_number_nan() {
456456
test("foo(Number.NaN)", "foo(NaN)");
457-
test_same("let Number; foo(Number.NaN)");
457+
test_same("var Number; foo(Number.NaN)");
458+
test_same("let Number; foo((void 0).NaN)");
458459
}
459460

460461
#[test]

0 commit comments

Comments
 (0)