Skip to content

Commit bb80763

Browse files
fix(minifier): scope rest parameter eval checks
1 parent 7919700 commit bb80763

8 files changed

Lines changed: 72 additions & 12 deletions

File tree

crates/swc_ecma_minifier/src/compress/optimize/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,8 @@ impl VisitMut for Optimizer<'_> {
23782378
n.decorators.visit_mut_with(self);
23792379

23802380
let old_in_asm = self.ctx.bit_ctx.contains(BitCtx::InAsm);
2381+
let function_is_strict =
2382+
self.ctx.expr_ctx.in_strict || rest_params::has_use_strict_directive(n);
23812383

23822384
{
23832385
let ctx = self.function_like_ctx(n.ctxt);
@@ -2420,7 +2422,8 @@ impl VisitMut for Optimizer<'_> {
24202422
}
24212423

24222424
{
2423-
self.with_ctx(self.ctx.clone()).drop_unused_rest_params(n);
2425+
self.with_ctx(self.ctx.clone())
2426+
.drop_unused_rest_params(n, function_is_strict);
24242427
}
24252428

24262429
self.ctx.bit_ctx.set(BitCtx::InAsm, old_in_asm);

crates/swc_ecma_minifier/src/compress/optimize/rest_params.rs

Lines changed: 41 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,37 @@
11
use swc_ecma_ast::*;
22
use swc_ecma_visit::{Visit, VisitWith};
33

4-
use super::{Optimizer, ProgramData, ScopeData, VarUsageInfoFlags};
4+
use super::{Optimizer, ProgramData, VarUsageInfoFlags};
5+
6+
/// Returns true if a function body has a strict-mode directive.
7+
pub(super) fn has_use_strict_directive(f: &Function) -> bool {
8+
f.body.as_ref().is_some_and(|body| {
9+
body.stmts
10+
.iter()
11+
.take_while(|stmt| {
12+
matches!(stmt, Stmt::Expr(ExprStmt { expr, .. }) if matches!(&**expr, Expr::Lit(Lit::Str(..))))
13+
})
14+
.any(|stmt| {
15+
matches!(stmt,
16+
Stmt::Expr(ExprStmt {
17+
expr,
18+
..
19+
}) if matches!(
20+
&**expr,
21+
Expr::Lit(Lit::Str(Str { raw: Some(raw), .. }))
22+
if raw == "\"use strict\"" || raw == "'use strict'"
23+
)
24+
)
25+
})
26+
})
27+
}
528

629
/// Returns true if a function body reads its implicit `arguments` object.
730
///
831
/// A lexical `arguments` declaration has a distinct syntax context, while a
932
/// `var arguments` declaration aliases the implicit arguments object unless it
1033
/// belongs to a nested arrow function.
1134
fn uses_implicit_arguments(f: &Function, data: &ProgramData) -> bool {
12-
// A direct eval can read `arguments` without a corresponding identifier node.
13-
if data
14-
.get_scope(f.ctxt)
15-
.is_some_and(|scope| scope.contains(ScopeData::HAS_EVAL_CALL))
16-
{
17-
return true;
18-
}
19-
2035
struct ArrowVarFinder {
2136
ids: Vec<Id>,
2237
}
@@ -92,6 +107,21 @@ fn uses_implicit_arguments(f: &Function, data: &ProgramData) -> bool {
92107
self.shadowed_arguments.truncate(shadowed_len);
93108
}
94109

110+
fn visit_call_expr(&mut self, call: &CallExpr) {
111+
// Direct eval can read the enclosing implicit `arguments` object without a
112+
// corresponding identifier node. Nested ordinary functions and constructors
113+
// are skipped above, while arrows share the enclosing lexical scope.
114+
if matches!(
115+
&call.callee,
116+
Callee::Expr(expr) if matches!(&**expr, Expr::Ident(Ident { sym, .. }) if *sym == *"eval")
117+
) && self.shadowed_arguments.is_empty()
118+
{
119+
self.found = true;
120+
}
121+
122+
call.visit_children_with(self);
123+
}
124+
95125
fn visit_ident(&mut self, ident: &Ident) {
96126
if ident.sym == "arguments"
97127
&& !self.shadowed_arguments.contains(&ident.to_id())
@@ -130,7 +160,7 @@ impl Optimizer<'_> {
130160
/// console.log(a);
131161
/// }
132162
/// ```
133-
pub(super) fn drop_unused_rest_params(&mut self, f: &mut Function) {
163+
pub(super) fn drop_unused_rest_params(&mut self, f: &mut Function, in_strict: bool) {
134164
if !self.options.arguments && !self.options.unused {
135165
return;
136166
}
@@ -161,7 +191,7 @@ impl Optimizer<'_> {
161191
// In that case, `arguments` becomes mapped to the remaining parameters, which
162192
// changes observable behavior when the function uses `arguments`.
163193
let can_make_arguments_mapped = f.params.len() > 1
164-
&& !self.ctx.expr_ctx.in_strict
194+
&& !in_strict
165195
&& f.params[..f.params.len() - 1].iter().all(|param| {
166196
matches!(
167197
&param.pat,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
original
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function f(value, ...rest) {
2+
"use strict";
3+
value = "changed";
4+
return arguments[0];
5+
}
6+
7+
console.log(f("original"));
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
function f(value) {
2+
"use strict";
3+
return arguments[0];
4+
}
5+
console.log(f("original"));
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
inner
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
function f(value, ...rest) {
2+
return function g() {
3+
return eval("arguments[0]");
4+
};
5+
}
6+
7+
console.log(f("outer")("inner"));
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
function f(value) {
2+
return function g() {
3+
return eval("arguments[0]");
4+
};
5+
}
6+
console.log(f("outer")("inner"));

0 commit comments

Comments
 (0)