Skip to content
Draft
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
121 changes: 121 additions & 0 deletions crates/swc_ecma_minifier/tests/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,45 @@ fn run_default_exec_test(input_src: &str) {
run_exec_test(input_src, config, false);
}

fn run_compress_and_mangle_exec_test(input_src: &str, config: &str) {
let expected_output = stdout_of(input_src).unwrap();

testing::run_test2(false, |cm, handler| {
let _tracing = span!(Level::ERROR, "compress-and-mangle").entered();

let output = run(
cm.clone(),
&handler,
input_src,
Some(config),
Some(MangleOptions {
top_level: Some(true),
..Default::default()
}),
);

let output = output.expect("Parsing in base test should not fail");
let output = print(cm, &[&output], true, false);

eprintln!(
"---- {} -----\n{}",
Color::Green.paint("Optimized code"),
output
);

let actual_output = stdout_of(&output).expect("failed to execute the optimized code");
assert_ne!(actual_output, "");

assert_eq!(
DebugUsingDisplay(&actual_output),
DebugUsingDisplay(&expected_output)
);

Ok(())
})
.unwrap();
}

#[test]
fn concat_tpl_keeps_delimiter_after_interpolation() {
run_default_exec_test(
Expand Down Expand Up @@ -12363,6 +12402,88 @@ console.log(out.poisoned);
run_default_exec_test(src);
}

#[test]
fn issue_11977_iife_param_extraction_does_not_collide_with_var() {
let src = r#"
var d = class {};
var __turbopack_context__ = {
z: function(value) {
return value;
}
};
var ErrorType = 1;
var i = {};
var serverOnlyRequire;
var Subscription = function() {
function Subscription1(listeners, listener) {}
Subscription1.prototype.add = function(subscription) {
if (this.unsubscribed) {}
};
}();
try {
serverOnlyRequire = eval('require');
} catch (err) {}
var __require = /* @__PURE__ */ ((x)=>("TURBOPACK compile-time truthy", 1) ? __turbopack_context__.z : "TURBOPACK unreachable")(function(x) {
})(ErrorType || {});
var isRequestError = (error)=>{
let u = i.getKey ?? ((p, s)=>`x`), f = async ()=>{
try {} catch (s) {}
};
};
var x = class extends d {};
console.log(typeof x, x === x);
"#;
let config = r#"{
"defaults": true,
"toplevel": true
}"#;

run_compress_and_mangle_exec_test(src, config);
}

#[test]
fn issue_11977_iife_param_extraction_does_not_collide_in_function() {
let src = r#"
var g = {};
var d = class {};
var __turbopack_context__ = {
z: function(value) {
return value;
}
};
var ErrorType = 1;
var i = {};
var serverOnlyRequire;
g.foo = function() {
var Subscription = function() {
function Subscription1(listeners, listener) {}
Subscription1.prototype.add = function(subscription) {
if (this.unsubscribed) {}
};
}();
try {
serverOnlyRequire = eval('require');
} catch (err) {}
var __require = /* @__PURE__ */ ((x)=>("TURBOPACK compile-time truthy", 1) ? __turbopack_context__.z : "TURBOPACK unreachable")(function(x) {
})(ErrorType || {});
var isRequestError = (error)=>{
let u = i.getKey ?? ((p, s)=>`x`), f = async ()=>{
try {} catch (s) {}
};
};
var x = class extends d {};
console.log(typeof x, x === x);
};
g.foo();
"#;
let config = r#"{
"defaults": true,
"toplevel": true
}"#;

run_compress_and_mangle_exec_test(src, config);
}

#[test]
fn issue_11294_eval_mangle_no_collision() {
// Regression test for #11294.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ impl Visit for AnalyzerAndCollector {
let old_decl_collector_is_pat_decl = self.decl_collector.is_pat_decl;
self.decl_collector.is_pat_decl = true;

let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.ctxt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve arrow parameters visible to eval

For arrows with direct eval, e.g. const f = (longName) => eval("longName"), the resolver creates a child function scope for the parameters but never marks ArrowExpr::ctxt; only a block body gets marked, and expression-bodied arrows have no marked body to use. Passing this empty context makes eval preservation compare the parameter context against SyntaxContext::empty(), so mangle can rename longName while eval still looks up the original string.

Useful? React with 👍 / 👎.

let old_analyzer_is_pat_decl = self.analyzer.is_pat_decl;
self.analyzer.is_pat_decl = true;

Expand Down Expand Up @@ -133,7 +133,7 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_block_stmt(&mut self, node: &BlockStmt) {
let old_analyzer = self.analyzer.enter_block_scope();
let old_analyzer = self.analyzer.enter_block_scope(Some(node.ctxt));

node.visit_children_with(self);

Expand All @@ -147,7 +147,7 @@ impl Visit for AnalyzerAndCollector {
fn visit_catch_clause(&mut self, node: &CatchClause) {
let old_decl_collector_is_pat_decl = self.decl_collector.is_pat_decl;

let old_analyzer = self.analyzer.enter_block_scope();
let old_analyzer = self.analyzer.enter_block_scope(Some(node.body.ctxt));
let old_analyzer_is_pat_decl = self.analyzer.is_pat_decl;
let old_analyzer_in_catch_params = self.analyzer.in_catch_params;

Expand Down Expand Up @@ -179,7 +179,7 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_class_expr(&mut self, node: &ClassExpr) {
let old_analyzer = self.analyzer.enter_block_scope();
let old_analyzer = self.analyzer.enter_block_scope(Some(node.class.ctxt));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve named class expressions seen by eval

With direct eval inside a named class expression, e.g. const C = class Foo { static m() { return eval("Foo") } }, this uses Class::ctxt as the source-scope context, but the resolver leaves Class::ctxt empty and marks the class-expression name in the child scope in visit_mut_class_expr. That makes id.1 == ctxt false for Foo, so the name is queued for mangling even though eval still resolves the string "Foo", regressing the eval-preservation behavior this code is trying to maintain.

Useful? React with 👍 / 👎.


self.analyzer.handle_class_expr(node);

Expand All @@ -193,7 +193,7 @@ impl Visit for AnalyzerAndCollector {
fn visit_class_method(&mut self, node: &ClassMethod) {
node.key.visit_with(self);

let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.function.ctxt);

node.function.decorators.visit_with(self);
node.function.params.visit_with(self);
Expand All @@ -205,7 +205,7 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_constructor(&mut self, node: &Constructor) {
let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.ctxt);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a marked constructor context for eval preservation

For class constructors with direct eval, such as class C { constructor(longName) { eval("longName") } }, Constructor::ctxt is never populated by the resolver; visit_mut_constructor creates a child function scope, marks parameters with that child mark, and only marks body.ctxt. Passing the empty node.ctxt here means constructor parameters and locals no longer match scope_ctxt, so they can be mangled while eval still looks up their original names.

Useful? React with 👍 / 👎.

node.key.visit_with(self);
node.params.visit_with(self);
if let Some(body) = &node.body {
Expand All @@ -221,7 +221,7 @@ impl Visit for AnalyzerAndCollector {
self.analyzer.handle_class_expr(c);
self.decl_collector.handle_class_expr(c);

let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(c.class.ctxt);
c.visit_children_with(self);
self.analyzer.exit_scope(old_analyzer);
}
Expand Down Expand Up @@ -293,7 +293,7 @@ impl Visit for AnalyzerAndCollector {

node.ident.visit_with(self);

let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.function.ctxt);

if !need_skip_analyzer_record && has_rest {
// self.analyer is different from above because we are in a function scope
Expand All @@ -312,9 +312,9 @@ impl Visit for AnalyzerAndCollector {

fn visit_fn_expr(&mut self, node: &FnExpr) {
if let Some(id) = &node.ident {
let old_analyzer0 = self.analyzer.enter_fn_scope();
let old_analyzer0 = self.analyzer.enter_fn_scope(id.ctxt);
self.analyzer.add_decl(id.to_id(), true);
let old_analyzer1 = self.analyzer.enter_fn_scope();
let old_analyzer1 = self.analyzer.enter_fn_scope(node.function.ctxt);
// https://github.com/swc-project/swc/issues/6819
//
// We need to check for assign pattern because safari has a bug.
Expand Down Expand Up @@ -348,10 +348,10 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_for_in_stmt(&mut self, node: &ForInStmt) {
let old_analyzer0 = self.analyzer.enter_block_scope();
let old_analyzer0 = self.analyzer.enter_block_scope(None);
node.left.visit_with(self);
node.right.visit_with(self);
let old_analyzer1 = self.analyzer.enter_block_scope();
let old_analyzer1 = self.analyzer.enter_block_scope(None);
match node.body.as_ref() {
Stmt::Block(n) => n.visit_children_with(self),
_ => node.body.visit_with(self),
Expand All @@ -361,10 +361,10 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_for_of_stmt(&mut self, node: &ForOfStmt) {
let old_analyzer0 = self.analyzer.enter_block_scope();
let old_analyzer0 = self.analyzer.enter_block_scope(None);
node.left.visit_with(self);
node.right.visit_with(self);
let old_analyzer1 = self.analyzer.enter_block_scope();
let old_analyzer1 = self.analyzer.enter_block_scope(None);
match node.body.as_ref() {
Stmt::Block(n) => n.visit_children_with(self),
_ => node.body.visit_with(self),
Expand All @@ -374,11 +374,11 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_for_stmt(&mut self, node: &ForStmt) {
let old_analyzer0 = self.analyzer.enter_block_scope();
let old_analyzer0 = self.analyzer.enter_block_scope(None);
node.init.visit_with(self);
node.test.visit_with(self);
node.update.visit_with(self);
let old_analyzer1 = self.analyzer.enter_block_scope();
let old_analyzer1 = self.analyzer.enter_block_scope(None);
match node.body.as_ref() {
Stmt::Block(n) => n.visit_children_with(self),
_ => node.body.visit_with(self),
Expand All @@ -388,7 +388,7 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_function(&mut self, node: &Function) {
let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.ctxt);

node.decorators.visit_with(self);
node.params.visit_with(self);
Expand Down Expand Up @@ -447,7 +447,7 @@ impl Visit for AnalyzerAndCollector {
}

fn visit_static_block(&mut self, node: &StaticBlock) {
let old_analyzer = self.analyzer.enter_fn_scope();
let old_analyzer = self.analyzer.enter_fn_scope(node.body.ctxt);

node.body.visit_children_with(self);

Expand Down
26 changes: 18 additions & 8 deletions crates/swc_ecma_transforms_base/src/rename/analyzer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use swc_common::Mark;
use swc_common::{Mark, SyntaxContext};
use swc_ecma_ast::*;

use self::scope::{Scope, ScopeKind};
Expand All @@ -18,6 +18,12 @@ pub(super) struct Analyzer {
pub(super) var_belong_to_fn_scope: bool,
pub(super) in_catch_params: bool,
pub(super) scope: Scope,
/// Context for declarations that belong directly to `scope`.
///
/// With direct `eval`, declarations created from the same source scope must
/// keep their printed names. Declarations moved from an inner scope are
/// still safe to rename, and the context lets us distinguish the two.
pub(super) scope_ctxt: Option<SyntaxContext>,
/// If we try add variables declared by `var` to the block scope,
/// variables will be added to `hoisted_vars` and merged to latest
/// function scope in the end.
Expand All @@ -43,6 +49,7 @@ impl Analyzer {
top_level_mark,
skip_first_fn_or_class_decl,
is_first_node: true,
scope_ctxt: Some(SyntaxContext::empty().apply_mark(top_level_mark)),
hoisted_vars: Vec::with_capacity(32),
mangle,
..Default::default()
Expand All @@ -53,12 +60,14 @@ impl Analyzer {
if belong_to_fn_scope {
match self.scope.kind {
ScopeKind::Fn => {
self.scope.add_decl(&id, self.has_eval, self.top_level_mark);
self.scope
.add_decl(&id, self.has_eval, self.top_level_mark, self.scope_ctxt);
}
ScopeKind::Block => self.hoisted_vars.push(id),
}
} else {
self.scope.add_decl(&id, self.has_eval, self.top_level_mark);
self.scope
.add_decl(&id, self.has_eval, self.top_level_mark, self.scope_ctxt);
}
}

Expand All @@ -85,10 +94,11 @@ impl Analyzer {
self.scope.reserve_usage(len);
}

fn enter_scope(&mut self, kind: ScopeKind) -> Self {
fn enter_scope(&mut self, kind: ScopeKind, scope_ctxt: Option<SyntaxContext>) -> Self {
let mut analyer = Analyzer {
has_eval: self.has_eval,
top_level_mark: self.top_level_mark,
scope_ctxt,
is_pat_decl: self.is_pat_decl,
var_belong_to_fn_scope: false,
in_catch_params: false,
Expand All @@ -105,12 +115,12 @@ impl Analyzer {
analyer // old analyzer
}

pub(super) fn enter_fn_scope(&mut self) -> Self {
self.enter_scope(ScopeKind::Fn)
pub(super) fn enter_fn_scope(&mut self, scope_ctxt: SyntaxContext) -> Self {
self.enter_scope(ScopeKind::Fn, Some(scope_ctxt))
}

pub(super) fn enter_block_scope(&mut self) -> Self {
self.enter_scope(ScopeKind::Block)
pub(super) fn enter_block_scope(&mut self, scope_ctxt: Option<SyntaxContext>) -> Self {
self.enter_scope(ScopeKind::Block, scope_ctxt)
}

pub(super) fn exit_scope(&mut self, mut v: Self) {
Expand Down
Loading
Loading