Skip to content

Commit

Permalink
fix(tree-shaking): detect export var side effects (#1579)
Browse files Browse the repository at this point in the history
* test: ✅ add a failure test

* fix: 🐛 add is pure detection in export var decl
  • Loading branch information
stormslowly authored Sep 11, 2024
1 parent 4897e32 commit f8a4380
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 79 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,7 @@ pub fn analyze_imports_and_exports(
local: named.local.to_string(),
imported: named.imported.as_ref().map(|i| match i {
ModuleExportName::Ident(i) => i.to_string(),
_ => panic!(
"non-ident imported is not supported when tree shaking"
),
_ => panic!("non-ident imported is not supported when tree shaking"),
}),
});
defined_idents.insert(named.local.to_string());
Expand All @@ -101,8 +99,7 @@ pub fn analyze_imports_and_exports(
continue;
}

specifiers
.push(ImportSpecifierInfo::Default(default.local.to_string()));
specifiers.push(ImportSpecifierInfo::Default(default.local.to_string()));
defined_idents.insert(default.local.to_string());
}
}
Expand Down Expand Up @@ -133,61 +130,73 @@ pub fn analyze_imports_and_exports(
span = export_decl.span;

match &export_decl.decl {
swc_ecma_ast::Decl::Class(class_decl) => {
exports = Some(ExportInfo {
source: None,
specifiers: vec![ExportSpecifierInfo::Named { local: class_decl.ident.to_string(), exported: None }],
stmt_id: *id,
});
defined_idents.insert(class_decl.ident.to_string());
analyze_and_insert_used_idents(&class_decl.class, Some(class_decl.ident.to_string()));
},
swc_ecma_ast::Decl::Fn(fn_decl) => {
exports = Some(ExportInfo {
source: None,
specifiers: vec![ExportSpecifierInfo::Named { local: fn_decl.ident.to_string(), exported: None }],
stmt_id: *id,
});
defined_idents.insert(fn_decl.ident.to_string());
analyze_and_insert_used_idents(&fn_decl.function, Some(fn_decl.ident.to_string()));
},
swc_ecma_ast::Decl::Var(var_decl) => {
let mut specifiers = vec![];

for v_decl in &var_decl.decls {

let mut defined_idents_collector = DefinedIdentsCollector::new();
v_decl.name.visit_with(&mut defined_idents_collector);
let mut used_idents_collector = UsedIdentsCollector::new();

if let Some(init) = &v_decl.init {
init.visit_with(&mut used_idents_collector);
}

let mut local_used_idents = HashSet::new();
local_used_idents.extend(used_idents_collector.used_idents);
local_used_idents.extend(defined_idents_collector.used_idents);
used_idents.extend(local_used_idents.clone());

for defined_ident in defined_idents_collector.defined_idents {
if !is_ident_used(&defined_ident.to_string()) {
continue;
}
swc_ecma_ast::Decl::Class(class_decl) => {
exports = Some(ExportInfo {
source: None,
specifiers: vec![ExportSpecifierInfo::Named {
local: class_decl.ident.to_string(),
exported: None,
}],
stmt_id: *id,
});
defined_idents.insert(class_decl.ident.to_string());
analyze_and_insert_used_idents(&class_decl.class, Some(class_decl.ident.to_string()));
}
swc_ecma_ast::Decl::Fn(fn_decl) => {
exports = Some(ExportInfo {
source: None,
specifiers: vec![ExportSpecifierInfo::Named {
local: fn_decl.ident.to_string(),
exported: None,
}],
stmt_id: *id,
});
defined_idents.insert(fn_decl.ident.to_string());
analyze_and_insert_used_idents(&fn_decl.function, Some(fn_decl.ident.to_string()));
}
swc_ecma_ast::Decl::Var(var_decl) => {
let mut specifiers = vec![];

specifiers.push(ExportSpecifierInfo::Named { local: defined_ident.to_string(), exported: None });
defined_idents.insert(defined_ident.clone());
defined_idents_map.insert(defined_ident.clone(), local_used_idents.clone());
}
}
is_self_executed = !is_pure_var_decl(var_decl, unresolve_ctxt);

exports = Some(ExportInfo {
source: None,
specifiers,
stmt_id: *id,
});
},
_ => unreachable!("export_decl.decl should not be anything other than a class, function, or variable declaration"),
}
for v_decl in &var_decl.decls {
let mut defined_idents_collector = DefinedIdentsCollector::new();
v_decl.name.visit_with(&mut defined_idents_collector);
let mut used_idents_collector = UsedIdentsCollector::new();

if let Some(init) = &v_decl.init {
init.visit_with(&mut used_idents_collector);
}

let mut local_used_idents = HashSet::new();
local_used_idents.extend(used_idents_collector.used_idents);
local_used_idents.extend(defined_idents_collector.used_idents);
used_idents.extend(local_used_idents.clone());

for defined_ident in defined_idents_collector.defined_idents {
if !is_ident_used(&defined_ident.to_string()) {
continue;
}

specifiers.push(ExportSpecifierInfo::Named {
local: defined_ident.to_string(),
exported: None,
});
defined_idents.insert(defined_ident.clone());
defined_idents_map.insert(defined_ident.clone(), local_used_idents.clone());
}
}

exports = Some(ExportInfo {
source: None,
specifiers,
stmt_id: *id,
});
}
_ => unreachable!(
"export_decl.decl should not be anything other than a class, function, or variable declaration"
),
}
}
swc_ecma_ast::ModuleDecl::ExportDefaultDecl(export_default_decl) => {
span = export_default_decl.span;
Expand Down Expand Up @@ -216,9 +225,7 @@ pub fn analyze_imports_and_exports(
fn_decl.ident.as_ref().map(|i| i.to_string()),
);
}
_ => unreachable!(
"export_default_decl.decl should not be anything other than a class, function"
),
_ => unreachable!("export_default_decl.decl should not be anything other than a class, function"),
}
}
swc_ecma_ast::ModuleDecl::ExportDefaultExpr(export_default_expr) => {
Expand Down Expand Up @@ -257,17 +264,14 @@ pub fn analyze_imports_and_exports(

if export_named.src.is_none() {
used_idents.insert(local.to_string());
defined_idents_map
.insert(local.to_string(), [local.to_string()].into());
defined_idents_map.insert(local.to_string(), [local.to_string()].into());
}

specifiers.push(ExportSpecifierInfo::Named {
local: local.to_string(),
exported: named.exported.as_ref().map(|i| match i {
ModuleExportName::Ident(i) => i.to_string(),
_ => panic!(
"non-ident exported is not supported when tree shaking"
),
_ => panic!("non-ident exported is not supported when tree shaking"),
}),
});
}
Expand Down Expand Up @@ -382,17 +386,11 @@ pub fn analyze_imports_and_exports(
swc_ecma_ast::Stmt::Decl(decl) => match decl {
swc_ecma_ast::Decl::Class(class_decl) => {
defined_idents.insert(class_decl.ident.to_string());
analyze_and_insert_used_idents(
&class_decl.class,
Some(class_decl.ident.to_string()),
);
analyze_and_insert_used_idents(&class_decl.class, Some(class_decl.ident.to_string()));
}
swc_ecma_ast::Decl::Fn(fn_decl) => {
defined_idents.insert(fn_decl.ident.to_string());
analyze_and_insert_used_idents(
&fn_decl.function,
Some(fn_decl.ident.to_string()),
);
analyze_and_insert_used_idents(&fn_decl.function, Some(fn_decl.ident.to_string()));
}
swc_ecma_ast::Decl::Var(var_decl) => {
for v_decl in &var_decl.decls {
Expand All @@ -411,18 +409,15 @@ pub fn analyze_imports_and_exports(

for defined_ident in defined_idents_collector.defined_idents {
defined_idents.insert(defined_ident.clone());
defined_idents_map
.insert(defined_ident.clone(), local_used_idents.clone());
defined_idents_map.insert(defined_ident.clone(), local_used_idents.clone());
}

if !is_pure_var_decl(var_decl, unresolve_ctxt) {
is_self_executed = true;
}
}
}
_ => unreachable!(
"decl should not be anything other than a class, function, or variable declaration"
),
_ => unreachable!("decl should not be anything other than a class, function, or variable declaration"),
},
swc_ecma_ast::Stmt::Expr(expr) => {
span = expr.span;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
max_width = 120
4 changes: 4 additions & 0 deletions e2e/fixtures/tree-shaking.export_var_decl/expect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const { injectSimpleJest } = require("../../../scripts/test-utils");

injectSimpleJest();
require("./dist/index.js");
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export var keep = record(42);
6 changes: 6 additions & 0 deletions e2e/fixtures/tree-shaking.export_var_decl/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { getStore } from "./recorder.js";
import "./export_var";

it("should keep side effects in export var", () => {
expect(getStore()).toStrictEqual([42]);
});
9 changes: 9 additions & 0 deletions e2e/fixtures/tree-shaking.export_var_decl/src/recorder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
let store = [];
global.record = function (n) {
store.push(n);
return n;
};

export function getStore() {
return store;
}

0 comments on commit f8a4380

Please sign in to comment.