Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/large-crabs-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
swc_ecma_minifier: patch
swc_core: patch
---

fix(es/minifier): More strict check if cannot add ident when invoking IIFE
114 changes: 54 additions & 60 deletions crates/swc_ecma_minifier/src/compress/optimize/iife.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use super::{util::NormalMultiReplacer, BitCtx, Optimizer};
#[cfg(feature = "debug")]
use crate::debug::dump;
use crate::{
program_data::{ProgramData, ScopeData, VarUsageInfoFlags},
program_data::{ProgramData, ScopeData, VarUsageInfo, VarUsageInfoFlags},
util::{idents_captured_by, make_number},
};

Expand Down Expand Up @@ -453,11 +453,6 @@ impl Optimizer<'_> {
_ => panic!("unable to access unknown nodes"),
};

if self.ctx.bit_ctx.contains(BitCtx::DontInvokeIife) {
log_abort!("iife: Inline is prevented");
return false;
}

for arg in &call.args {
if arg.spread.is_some() {
log_abort!("iife: Found spread argument");
Expand Down Expand Up @@ -580,8 +575,7 @@ impl Optimizer<'_> {

match &mut *f.body {
BlockStmtOrExpr::BlockStmt(body) => {
let new =
self.inline_fn_like(param_ids, f.params.len(), body, &mut call.args);
let new = self.inline_fn_like(param_ids, body, &mut call.args);
if let Some(new) = new {
self.changed = true;
report_change!("inline: Inlining a function call (arrow)");
Expand All @@ -590,7 +584,7 @@ impl Optimizer<'_> {
}
}
BlockStmtOrExpr::Expr(body) => {
if !self.can_extract_param(param_ids.clone()) {
if !self.can_extract_param(param_ids.clone(), &call.args) {
return;
}

Expand All @@ -605,12 +599,7 @@ impl Optimizer<'_> {

let mut exprs = vec![Box::new(make_number(DUMMY_SP, 0.0))];

let vars = self.inline_fn_param(
param_ids,
f.params.len(),
&mut call.args,
&mut exprs,
);
let vars = self.inline_fn_param(param_ids, &mut call.args, &mut exprs);

if !vars.is_empty() {
self.prepend_stmts.push(
Expand Down Expand Up @@ -663,8 +652,7 @@ impl Optimizer<'_> {
#[cfg(feature = "debug")]
let param_ids_for_debug = param_ids.clone();

let new =
self.inline_fn_like(param_ids, f.function.params.len(), body, &mut call.args);
let new = self.inline_fn_like(param_ids, body, &mut call.args);
if let Some(new) = new {
self.changed = true;
report_change!(
Expand Down Expand Up @@ -718,7 +706,6 @@ impl Optimizer<'_> {
let param_ids = f.params.iter().map(|p| &p.as_ident().unwrap().id);
self.inline_fn_like_stmt(
param_ids,
f.params.len(),
block_stmt,
&mut call.args,
is_return,
Expand All @@ -736,14 +723,7 @@ impl Optimizer<'_> {
.params
.iter()
.map(|p| &p.pat.as_ident().unwrap().id);
self.inline_fn_like_stmt(
param_ids,
f.function.params.len(),
body,
&mut call.args,
is_return,
call.span,
)
self.inline_fn_like_stmt(param_ids, body, &mut call.args, is_return, call.span)
}
_ => None,
}
Expand All @@ -770,14 +750,39 @@ impl Optimizer<'_> {
}
}

fn can_extract_param<'a>(&self, param_ids: impl Iterator<Item = &'a Ident> + Clone) -> bool {
fn can_inline_fn_arg(usage: &VarUsageInfo, arg: &Expr) -> bool {
if usage.ref_count > 1
|| usage.assign_count > 0
|| usage.property_mutation_count > 0
|| usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
|| usage.flags.contains(VarUsageInfoFlags::INLINE_PREVENTED)
{
return false;
}

match arg {
Expr::Lit(
Lit::Num(..) | Lit::Str(..) | Lit::Bool(..) | Lit::Null(..) | Lit::BigInt(..),
) => true,
Expr::Fn(..) | Expr::Arrow(..) if usage.can_inline_fn_once() => true,
_ => false,
}
}

fn can_extract_param<'a>(
&self,
param_ids: impl ExactSizeIterator<Item = &'a Ident> + Clone,
args: &[ExprOrSpread],
) -> bool {
// Don't create top-level variables.
if !self.may_add_ident() {
for pid in param_ids.clone() {
for (idx, pid) in param_ids.clone().enumerate() {
if let Some(usage) = self.data.vars.get(&pid.to_id()) {
if usage.ref_count > 1
|| usage.assign_count > 0
|| usage.flags.contains(VarUsageInfoFlags::INLINE_PREVENTED)
let arg = args.get(idx).map(|a| &*a.expr);

if !arg
.map(|a| Self::can_inline_fn_arg(usage, a))
.unwrap_or(false)
{
log_abort!("iife: [x] Cannot inline because of usage of `{}`", pid);
return false;
Expand Down Expand Up @@ -875,8 +880,8 @@ impl Optimizer<'_> {

fn can_inline_fn_like<'a>(
&self,
param_ids: impl Iterator<Item = &'a Ident> + Clone,
params_len: usize,
param_ids: impl ExactSizeIterator<Item = &'a Ident> + Clone,
args: &[ExprOrSpread],
body: &BlockStmt,
for_stmt: bool,
) -> bool {
Expand All @@ -890,7 +895,7 @@ impl Optimizer<'_> {
}
}

if !self.can_extract_param(param_ids.clone()) {
if !self.can_extract_param(param_ids.clone(), args) {
return false;
}

Expand Down Expand Up @@ -919,7 +924,7 @@ impl Optimizer<'_> {
}

if self.ctx.bit_ctx.contains(BitCtx::ExecutedMultipleTime) {
if params_len != 0 {
if param_ids.len() != 0 {
let captured = idents_captured_by(body);

for param in param_ids {
Expand Down Expand Up @@ -953,12 +958,11 @@ impl Optimizer<'_> {

fn inline_fn_param<'a>(
&mut self,
params: impl Iterator<Item = &'a Ident>,
params_len: usize,
params: impl ExactSizeIterator<Item = &'a Ident>,
args: &mut [ExprOrSpread],
exprs: &mut Vec<Box<Expr>>,
) -> Vec<VarDeclarator> {
let mut vars = Vec::with_capacity(params_len);
let mut vars = Vec::with_capacity(params.len());

for (idx, param) in params.enumerate() {
let arg = args.get_mut(idx).map(|arg| arg.expr.take());
Expand Down Expand Up @@ -1013,27 +1017,17 @@ impl Optimizer<'_> {

fn inline_fn_param_stmt<'a>(
&mut self,
params: impl Iterator<Item = &'a Ident>,
params_len: usize,
params: impl ExactSizeIterator<Item = &'a Ident>,
args: &mut [ExprOrSpread],
) -> Vec<VarDeclarator> {
let mut vars = Vec::with_capacity(params_len);
let mut vars = Vec::with_capacity(params.len());

for (idx, param) in params.enumerate() {
let mut arg = args.get_mut(idx).map(|arg| arg.expr.take());

if let Some(arg) = &mut arg {
if let Some(usage) = self.data.vars.get_mut(&param.to_id()) {
if usage.ref_count == 1
&& !usage.flags.contains(VarUsageInfoFlags::REASSIGNED)
&& usage.property_mutation_count == 0
&& matches!(
&**arg,
Expr::Lit(
Lit::Num(..) | Lit::Str(..) | Lit::Bool(..) | Lit::BigInt(..)
)
)
{
if Self::can_inline_fn_arg(usage, arg) {
// We don't need to create a variable in this case
self.vars
.vars_for_inlining
Expand Down Expand Up @@ -1125,21 +1119,21 @@ impl Optimizer<'_> {

fn inline_fn_like<'a>(
&mut self,
params: impl Iterator<Item = &'a Ident> + Clone,
params_len: usize,
params: impl ExactSizeIterator<Item = &'a Ident> + Clone,
body: &mut BlockStmt,
args: &mut [ExprOrSpread],
) -> Option<Expr> {
if !self.can_inline_fn_like(params.clone(), params_len, &*body, false) {
if !self.can_inline_fn_like(params.clone(), args, &*body, false) {
return None;
}

if self.vars.inline_with_multi_replacer(body) {
self.changed = true;
}

let params_len = params.len();
let mut exprs = Vec::new();
let vars = self.inline_fn_param(params, params_len, args, &mut exprs);
let vars = self.inline_fn_param(params, args, &mut exprs);

if args.len() > params_len {
for arg in &mut args[params_len..] {
Expand Down Expand Up @@ -1190,14 +1184,13 @@ impl Optimizer<'_> {

fn inline_fn_like_stmt<'a>(
&mut self,
params: impl Iterator<Item = &'a Ident> + Clone + std::fmt::Debug,
params_len: usize,
params: impl ExactSizeIterator<Item = &'a Ident> + Clone + std::fmt::Debug,
body: &mut BlockStmt,
args: &mut [ExprOrSpread],
is_return: bool,
span: Span,
) -> Option<BlockStmt> {
if !self.can_inline_fn_like(params.clone(), params_len, body, true) {
if !self.can_inline_fn_like(params.clone(), args, body, true) {
return None;
}

Expand All @@ -1210,7 +1203,7 @@ impl Optimizer<'_> {
}

if decl.count
+ (params_len.saturating_sub(
+ (params.len().saturating_sub(
args.iter()
.filter(|a| {
a.expr.is_ident() || a.expr.as_lit().map(|l| !l.is_regex()).unwrap_or(false)
Expand All @@ -1237,7 +1230,8 @@ impl Optimizer<'_> {

let mut stmts = Vec::with_capacity(body.stmts.len() + 2);

let param_decl = self.inline_fn_param_stmt(params, params_len, args);
let params_len = params.len();
let param_decl = self.inline_fn_param_stmt(params, args);

if !param_decl.is_empty() {
let param_decl = Stmt::Decl(Decl::Var(Box::new(VarDecl {
Expand Down
14 changes: 4 additions & 10 deletions crates/swc_ecma_minifier/src/compress/optimize/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,14 +181,12 @@ bitflags! {

const IsNestedIfReturnMerging = 1 << 24;

const DontInvokeIife = 1 << 25;
const InWithStmt = 1 << 25;

const InWithStmt = 1 << 26;

const InParam = 1 << 27;
const InParam = 1 << 26;

/// `true` while we are inside a class body.
const InClass = 1 << 28;
const InClass = 1 << 27;
}
}

Expand Down Expand Up @@ -1702,11 +1700,7 @@ impl VisitMut for Optimizer<'_> {
n.decorators.visit_mut_with(self);

{
let ctx = self
.ctx
.clone()
.with(BitCtx::DontInvokeIife, true)
.with(BitCtx::IsUpdateArg, false);
let ctx = self.ctx.clone().with(BitCtx::IsUpdateArg, false);
n.super_class.visit_mut_with(&mut *self.with_ctx(ctx));
}

Expand Down
33 changes: 33 additions & 0 deletions crates/swc_ecma_minifier/tests/fixture/issues/11368/input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* SWC Compress Bug - Playground Reproduction
* ==========================================
*
*
* Steps:
* 1. Run the output → logs "BUG: expected A, got B"
* 2. Disable compress and run again → logs "OK: got A"
*
* Bug: When SWC's compress is enabled, a class property that calls
* a function which returns a closure will have that closure capture
* values from the LAST instance instead of its own.
*/

const wrap = (cb) => () => cb();

class Base {
constructor(props) {
this.props = props;
}
}

class C extends Base {
fn = wrap(this.props.cb);
}

// Test
const a = new C({ cb: () => "A" });
const b = new C({ cb: () => "B" });

const result = a.fn();

console.log(result === "A" ? "OK: got A" : "BUG: expected A, got " + result);
29 changes: 29 additions & 0 deletions crates/swc_ecma_minifier/tests/fixture/issues/11368/output.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* SWC Compress Bug - Playground Reproduction
* ==========================================
*
*
* Steps:
* 1. Run the output → logs "BUG: expected A, got B"
* 2. Disable compress and run again → logs "OK: got A"
*
* Bug: When SWC's compress is enabled, a class property that calls
* a function which returns a closure will have that closure capture
* values from the LAST instance instead of its own.
*/ class Base {
constructor(props){
this.props = props;
}
}
class C extends Base {
fn = ((cb)=>()=>cb())(this.props.cb);
}
// Test
const a = new C({
cb: ()=>"A"
});
new C({
cb: ()=>"B"
});
const result = a.fn();
console.log("A" === result ? "OK: got A" : "BUG: expected A, got " + result);
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
var b = 1;
console.log(b-- && ++b);
console.log(function(a) {
return a && ++b;
}(b--));
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
var b;
x(b);
(function(a) {
x(a);
})(b);
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var a = 1;
a++, a && a.var, a++;
!function(a_1) {
a++;
}((a++, a && a.var));
console.log(a);
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
var a = 1;
a++, a && a.var, a++;
!function(a_1) {
a++;
}((a++, a && a.var));
console.log(a);
Loading