From 0e804aa8ac2a7ac0cbc3c8f10a9a009d83e09ba3 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:42:51 +0000 Subject: [PATCH] fix(minifier): keep variables that are modified by combined assignments made by minification (#13267) ```js (function() { let v; window.foo = function() { return v ?? (v = bar()); } })() ``` was compressed to ```js (function() { window.foo = function() { return bar(); } })() ``` . But this changes the semantics because `bar()` will be executed every time `window.foo()` is called after the minification. This was caused by not updating the semantics information when compressing `v ?? (v = bar())` into `v ??= bar()` (and other similar ones). This PR fixes that by updating the semantics information when those compressions are applied. --- .../src/peephole/minimize_conditions.rs | 7 ++ .../peephole/minimize_logical_expression.rs | 17 +++++ crates/oxc_minifier/tests/peephole/oxc.rs | 75 +++++++++++++++++++ 3 files changed, 99 insertions(+) diff --git a/crates/oxc_minifier/src/peephole/minimize_conditions.rs b/crates/oxc_minifier/src/peephole/minimize_conditions.rs index d6f69bbd7ebfe..fc929b9a4a3fb 100644 --- a/crates/oxc_minifier/src/peephole/minimize_conditions.rs +++ b/crates/oxc_minifier/src/peephole/minimize_conditions.rs @@ -4,6 +4,7 @@ use oxc_ecmascript::{ constant_evaluation::{ConstantEvaluation, ConstantValue, DetermineValueType}, side_effects::MayHaveSideEffects, }; +use oxc_semantic::ReferenceFlags; use oxc_span::GetSpan; use oxc_syntax::es_target::ESTarget; @@ -200,6 +201,9 @@ impl<'a> PeepholeOptimizations { return; } + let reference = ctx.scoping_mut().get_reference_mut(write_id_ref.reference_id()); + reference.flags_mut().insert(ReferenceFlags::Read); + let new_op = logical_expr.operator.to_assignment_operator(); expr.operator = new_op; expr.right = logical_expr.right.take_in(ctx.ast); @@ -220,6 +224,9 @@ impl<'a> PeepholeOptimizations { { return; } + + Self::mark_assignment_target_as_read(&expr.left, ctx); + expr.operator = new_op; expr.right = binary_expr.right.take_in(ctx.ast); ctx.state.changed = true; diff --git a/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs b/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs index 452dca55d8635..26483b0e6f017 100644 --- a/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs +++ b/crates/oxc_minifier/src/peephole/minimize_logical_expression.rs @@ -1,5 +1,6 @@ use oxc_allocator::TakeIn; use oxc_ast::ast::*; +use oxc_semantic::ReferenceFlags; use oxc_span::{ContentEq, GetSpan}; use oxc_syntax::es_target::ESTarget; @@ -236,6 +237,8 @@ impl<'a> PeepholeOptimizations { unreachable!() }; + Self::mark_assignment_target_as_read(&assignment_expr.left, ctx); + let assign_value = assignment_expr.right.take_in(ctx.ast); sequence_expr.expressions.push(assign_value); *expr = ctx.ast.expression_assignment( @@ -259,6 +262,9 @@ impl<'a> PeepholeOptimizations { { return; } + + Self::mark_assignment_target_as_read(&assignment_expr.left, ctx); + let span = e.span; let Expression::AssignmentExpression(assignment_expr) = &mut e.right else { return; @@ -268,4 +274,15 @@ impl<'a> PeepholeOptimizations { *expr = e.right.take_in(ctx.ast); ctx.state.changed = true; } + + /// Marks the AssignmentTargetIdentifier of assignment expressions as ReferenceFlags::Read + /// + /// When creating AssignmentTargetIdentifier from normal expressions, the identifier only has ReferenceFlags::Write. + /// But assignment expressions changes the value, so we should add ReferenceFlags::Read. + pub fn mark_assignment_target_as_read(assign_target: &AssignmentTarget, ctx: &mut Ctx<'a, '_>) { + if let AssignmentTarget::AssignmentTargetIdentifier(id) = assign_target { + let reference = ctx.scoping_mut().get_reference_mut(id.reference_id()); + reference.flags_mut().insert(ReferenceFlags::Read); + } + } } diff --git a/crates/oxc_minifier/tests/peephole/oxc.rs b/crates/oxc_minifier/tests/peephole/oxc.rs index ef663a7aef0ca..793aee7376fba 100644 --- a/crates/oxc_minifier/tests/peephole/oxc.rs +++ b/crates/oxc_minifier/tests/peephole/oxc.rs @@ -1,6 +1,13 @@ +use oxc_minifier::CompressOptions; + +use super::super::test as test_options; /// Oxc Integration Tests use super::{test, test_same}; +fn test_unused(source_text: &str, expected: &str) { + test_options(source_text, expected, CompressOptions::default()); +} + #[test] fn integration() { test( @@ -104,3 +111,71 @@ fn eval() { test_same("eval?.(x, y)"); test_same("eval?.(x,y)"); } + +#[test] +fn unused() { + test_unused( + "(function() { + let v; + window.foo = function() { + return v ?? (v = bar()); + } + })() + ", + "(function() { + let v; + window.foo = function() { + return v ??= bar(); + } + })() + ", + ); + test_unused( + "(function() { + let v; + window.foo = function() { + return v ?? (console.log(), v = bar()); + } + })() + ", + "(function() { + let v; + window.foo = function() { + return v ??= (console.log(), bar()); + } + })() + ", + ); + test_unused( + "(function() { + let v; + window.foo = function() { + return v = v || bar(); + } + })() + ", + "(function() { + let v; + window.foo = function() { + return v ||= bar(); + } + })() + ", + ); + test_unused( + "(function() { + let v; + window.foo = function() { + return v = v + bar(); + } + })() + ", + "(function() { + let v; + window.foo = function() { + return v += bar(); + } + })() + ", + ); +}