From 3ad07a7d2e5c9507a786dc338f0cf50191916aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Wed, 21 Jun 2023 16:25:29 +0900 Subject: [PATCH] feat(es/minifier): Support `__NO_SIDE_EFFECTS__` (#7532) **Related issue:** - Closes #7525. --- crates/swc_ecma_minifier/src/metadata/mod.rs | 370 ++++++------------ .../tests/fixture/no-side-effect/config.json | 5 + .../tests/fixture/no-side-effect/input.js | 77 ++++ .../output.js | 2 +- .../output.js | 2 +- 5 files changed, 202 insertions(+), 254 deletions(-) create mode 100644 crates/swc_ecma_minifier/tests/fixture/no-side-effect/config.json create mode 100644 crates/swc_ecma_minifier/tests/fixture/no-side-effect/input.js diff --git a/crates/swc_ecma_minifier/src/metadata/mod.rs b/crates/swc_ecma_minifier/src/metadata/mod.rs index de4541a51125..c6a5636239fd 100644 --- a/crates/swc_ecma_minifier/src/metadata/mod.rs +++ b/crates/swc_ecma_minifier/src/metadata/mod.rs @@ -1,11 +1,11 @@ use rustc_hash::FxHashSet; use swc_common::{ comments::{Comment, CommentKind, Comments}, - Span, SyntaxContext, + Span, Spanned, }; use swc_ecma_ast::*; use swc_ecma_usage_analyzer::marks::Marks; -use swc_ecma_utils::{find_pat_ids, NodeIgnoringSpan}; +use swc_ecma_utils::NodeIgnoringSpan; use swc_ecma_visit::{ noop_visit_mut_type, noop_visit_type, Visit, VisitMut, VisitMutWith, VisitWith, }; @@ -36,6 +36,7 @@ pub(crate) fn info_marker<'a>( pure_funcs, // unresolved_mark, state: Default::default(), + pure_callee: Default::default(), } } @@ -49,95 +50,36 @@ struct InfoMarker<'a> { #[allow(dead_code)] options: Option<&'a CompressOptions>, pure_funcs: Option>>, + pure_callee: FxHashSet, + comments: Option<&'a dyn Comments>, marks: Marks, // unresolved_mark: Mark, state: State, } -impl InfoMarker<'_> { - /// Check for `/** @const */`. - pub(super) fn has_const_ann(&self, span: Span) -> bool { - self.find_comment(span, |c| { - if c.kind == CommentKind::Block { - if !c.text.starts_with('*') { - return false; - } - let t = c.text[1..].trim(); - // - if t.starts_with("@const") { - return true; - } - } - - false - }) - } - - /// Check for `/*#__NOINLINE__*/` - pub(super) fn has_noinline(&self, span: Span) -> bool { - self.has_flag(span, "NOINLINE") - } - - /// Check for `/*#__PURE__*/` - pub(super) fn has_pure(&self, span: Span) -> bool { - self.has_flag(span, "PURE") - } - - fn find_comment(&self, span: Span, mut op: F) -> bool - where - F: FnMut(&Comment) -> bool, - { - let mut found = false; - if let Some(comments) = self.comments { - let cs = comments.get_leading(span.lo); - if let Some(cs) = cs { - for c in &cs { - found |= op(c); - if found { - break; - } - } - } - } - - found - } - - fn has_flag(&self, span: Span, text: &'static str) -> bool { - self.find_comment(span, |c| { - if c.kind == CommentKind::Block { - // - if c.text.len() == (text.len() + 5) - && (c.text.starts_with("#__") || c.text.starts_with("@__")) - && c.text.ends_with("__") - && text == &c.text[3..c.text.len() - 2] - { - return true; - } - } - - false - }) - } -} - impl VisitMut for InfoMarker<'_> { noop_visit_mut_type!(); fn visit_mut_call_expr(&mut self, n: &mut CallExpr) { n.visit_mut_children_with(self); - if self.has_noinline(n.span) { + if has_noinline(self.comments, n.span) { n.span = n.span.apply_mark(self.marks.noinline); } // We check callee in some cases because we move comments // See https://github.com/swc-project/swc/issues/7241 - if self.has_pure(n.span) + if match &n.callee { + Callee::Expr(e) => match &**e { + Expr::Ident(callee) => self.pure_callee.contains(&callee.to_id()), + _ => false, + }, + _ => false, + } || has_pure(self.comments, n.span) || match &n.callee { Callee::Expr(e) => match &**e { - Expr::Seq(callee) => self.has_pure(callee.span), + Expr::Seq(callee) => has_pure(self.comments, callee.span), _ => false, }, _ => false, @@ -202,6 +144,11 @@ impl VisitMut for InfoMarker<'_> { fn visit_mut_lit(&mut self, _: &mut Lit) {} fn visit_mut_module(&mut self, n: &mut Module) { + n.visit_with(&mut InfoCollector { + comments: self.comments, + pure_callees: &mut self.pure_callee, + }); + n.visit_mut_children_with(self); if self.state.is_bundle { @@ -215,12 +162,17 @@ impl VisitMut for InfoMarker<'_> { fn visit_mut_new_expr(&mut self, n: &mut NewExpr) { n.visit_mut_children_with(self); - if self.has_pure(n.span) { + if has_pure(self.comments, n.span) { n.span = n.span.apply_mark(self.marks.pure); } } fn visit_mut_script(&mut self, n: &mut Script) { + n.visit_with(&mut InfoCollector { + comments: self.comments, + pure_callees: &mut self.pure_callee, + }); + n.visit_mut_children_with(self); if self.state.is_bundle { @@ -234,7 +186,7 @@ impl VisitMut for InfoMarker<'_> { fn visit_mut_var_decl(&mut self, n: &mut VarDecl) { n.visit_mut_children_with(self); - if self.has_const_ann(n.span) { + if has_const_ann(self.comments, n.span) { n.span = n.span.apply_mark(self.marks.const_ann); } } @@ -247,216 +199,130 @@ fn is_param_one_of(p: &Param, allowed: &[&str]) -> bool { } } -struct TopLevelBindingCollector { - top_level_ctxt: SyntaxContext, - bindings: Vec, -} +const NO_SIDE_EFFECTS_FLAG: &str = "NO_SIDE_EFFECTS"; -impl TopLevelBindingCollector { - fn add(&mut self, id: Id) { - if id.1 != self.top_level_ctxt { - return; - } +struct InfoCollector<'a> { + comments: Option<&'a dyn Comments>, - self.bindings.push(id); - } + pure_callees: &'a mut FxHashSet, } -impl Visit for TopLevelBindingCollector { +impl Visit for InfoCollector<'_> { noop_visit_type!(); - fn visit_class_decl(&mut self, v: &ClassDecl) { - self.add(v.ident.to_id()); - } - - fn visit_fn_decl(&mut self, v: &FnDecl) { - self.add(v.ident.to_id()); - } - - fn visit_function(&mut self, _: &Function) {} + fn visit_export_decl(&mut self, f: &ExportDecl) { + f.visit_children_with(self); - fn visit_var_decl(&mut self, v: &VarDecl) { - v.visit_children_with(self); - let ids: Vec = find_pat_ids(&v.decls); - - for id in ids { - self.add(id) + if let Decl::Fn(f) = &f.decl { + if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) { + self.pure_callees.insert(f.ident.to_id()); + } } } -} - -// fn is_standalone(n: &mut N, unresolved_mark: Mark) -> bool -// where -// N: VisitMutWith, -// { -// let unresolved_ctxt = SyntaxContext::empty().apply_mark(unresolved_mark); - -// let bindings = { -// let mut v = IdentCollector { -// ids: Default::default(), -// for_binding: true, -// is_pat_decl: false, -// }; -// n.visit_mut_with(&mut v); -// v.ids -// }; - -// let used = { -// let mut v = IdentCollector { -// ids: Default::default(), -// for_binding: false, -// is_pat_decl: false, -// }; -// n.visit_mut_with(&mut v); -// v.ids -// }; - -// for used_id in &used { -// if used_id.0.starts_with("__WEBPACK_EXTERNAL_MODULE_") { -// continue; -// } - -// match &*used_id.0 { -// "__webpack_require__" | "exports" => continue, -// _ => {} -// } - -// if used_id.1 == unresolved_ctxt { -// // if cfg!(feature = "debug") { -// // debug!("bundle: Ignoring {}{:?} (top level)", used_id.0, -// // used_id.1); } -// continue; -// } - -// if bindings.contains(used_id) { -// // if cfg!(feature = "debug") { -// // debug!( -// // "bundle: Ignoring {}{:?} (local to fn)", -// // used_id.0, -// // used_id.1 -// // ); -// // } -// continue; -// } - -// trace_op!( -// "bundle: Due to {}{:?}, it's not a bundle", -// used_id.0, -// used_id.1 -// ); - -// return false; -// } - -// true -// } - -struct IdentCollector { - ids: Vec, - for_binding: bool, - - is_pat_decl: bool, -} - -impl IdentCollector { - fn add(&mut self, i: &Ident) { - let id = i.to_id(); - self.ids.push(id); - } -} -impl VisitMut for IdentCollector { - noop_visit_mut_type!(); - - fn visit_mut_catch_clause(&mut self, c: &mut CatchClause) { - let old = self.is_pat_decl; - self.is_pat_decl = true; - c.param.visit_mut_children_with(self); - self.is_pat_decl = old; - - self.is_pat_decl = false; - c.body.visit_mut_with(self); - - self.is_pat_decl = old; - } + fn visit_fn_decl(&mut self, f: &FnDecl) { + f.visit_children_with(self); - fn visit_mut_class_decl(&mut self, e: &mut ClassDecl) { - if self.for_binding { - e.ident.visit_mut_with(self); + if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) { + self.pure_callees.insert(f.ident.to_id()); } - - e.class.visit_mut_with(self); } - fn visit_mut_class_expr(&mut self, e: &mut ClassExpr) { - e.class.visit_mut_with(self); - } + fn visit_fn_expr(&mut self, f: &FnExpr) { + f.visit_children_with(self); - fn visit_mut_expr(&mut self, e: &mut Expr) { - match e { - Expr::Ident(..) if self.for_binding => {} - _ => { - e.visit_mut_children_with(self); + if let Some(ident) = &f.ident { + if has_flag(self.comments, f.function.span, NO_SIDE_EFFECTS_FLAG) { + self.pure_callees.insert(ident.to_id()); } } } - fn visit_mut_fn_decl(&mut self, e: &mut FnDecl) { - if self.for_binding { - e.ident.visit_mut_with(self); - } - - e.function.visit_mut_with(self); - } + fn visit_var_decl(&mut self, decl: &VarDecl) { + decl.visit_children_with(self); - fn visit_mut_fn_expr(&mut self, e: &mut FnExpr) { - if self.for_binding { - e.ident.visit_mut_with(self); + for v in &decl.decls { + if let Pat::Ident(ident) = &v.name { + if let Some(init) = &v.init { + if has_flag(self.comments, decl.span, NO_SIDE_EFFECTS_FLAG) + || has_flag(self.comments, v.span, NO_SIDE_EFFECTS_FLAG) + || has_flag(self.comments, init.span(), NO_SIDE_EFFECTS_FLAG) + { + self.pure_callees.insert(ident.to_id()); + } + } + } } - - e.function.visit_mut_with(self); } +} - fn visit_mut_ident(&mut self, i: &mut Ident) { - self.add(&*i); - } +/// Check for `/** @const */`. +pub(super) fn has_const_ann(comments: Option<&dyn Comments>, span: Span) -> bool { + find_comment(comments, span, |c| { + if c.kind == CommentKind::Block { + if !c.text.starts_with('*') { + return false; + } + let t = c.text[1..].trim(); + // + if t.starts_with("@const") { + return true; + } + } - fn visit_mut_labeled_stmt(&mut self, s: &mut LabeledStmt) { - s.body.visit_mut_with(self); - } + false + }) +} - fn visit_mut_param(&mut self, p: &mut Param) { - let old = self.is_pat_decl; - self.is_pat_decl = true; - p.visit_mut_children_with(self); - self.is_pat_decl = old; - } +/// Check for `/*#__NOINLINE__*/` +pub(super) fn has_noinline(comments: Option<&dyn Comments>, span: Span) -> bool { + has_flag(comments, span, "NOINLINE") +} - fn visit_mut_pat(&mut self, p: &mut Pat) { - match p { - Pat::Ident(..) if self.for_binding && !self.is_pat_decl => {} +/// Check for `/*#__PURE__*/` +pub(super) fn has_pure(comments: Option<&dyn Comments>, span: Span) -> bool { + has_flag(comments, span, "PURE") +} - _ => { - p.visit_mut_children_with(self); +fn find_comment(comments: Option<&dyn Comments>, span: Span, mut op: F) -> bool +where + F: FnMut(&Comment) -> bool, +{ + let mut found = false; + if let Some(comments) = comments { + let cs = comments.get_leading(span.lo); + if let Some(cs) = cs { + for c in &cs { + found |= op(c); + if found { + break; + } } } } - fn visit_mut_prop_name(&mut self, p: &mut PropName) { - if let PropName::Computed(..) = p { - p.visit_mut_children_with(self); - } - } - - fn visit_mut_var_declarator(&mut self, d: &mut VarDeclarator) { - let old = self.is_pat_decl; + found +} - self.is_pat_decl = true; - d.name.visit_mut_with(self); +fn has_flag(comments: Option<&dyn Comments>, span: Span, text: &'static str) -> bool { + find_comment(comments, span, |c| { + if c.kind == CommentKind::Block { + for line in c.text.lines() { + // jsdoc + let line = line.trim_start_matches(['*', ' ']); + let line = line.trim(); - self.is_pat_decl = false; - d.init.visit_mut_with(self); + // + if line.len() == (text.len() + 5) + && (line.starts_with("#__") || line.starts_with("@__")) + && line.ends_with("__") + && text == &line[3..line.len() - 2] + { + return true; + } + } + } - self.is_pat_decl = old; - } + false + }) } diff --git a/crates/swc_ecma_minifier/tests/fixture/no-side-effect/config.json b/crates/swc_ecma_minifier/tests/fixture/no-side-effect/config.json new file mode 100644 index 000000000000..ac4e89404c80 --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/no-side-effect/config.json @@ -0,0 +1,5 @@ +{ + "defaults": true, + "toplevel": true, + "passes": 0 +} diff --git a/crates/swc_ecma_minifier/tests/fixture/no-side-effect/input.js b/crates/swc_ecma_minifier/tests/fixture/no-side-effect/input.js new file mode 100644 index 000000000000..7fbe8a1f73ab --- /dev/null +++ b/crates/swc_ecma_minifier/tests/fixture/no-side-effect/input.js @@ -0,0 +1,77 @@ +/*#__NO_SIDE_EFFECTS__*/ +function fnA(args) { + // ... + const a = console.log('AAA') + console.log(a) +} + +const fnB = /*#__NO_SIDE_EFFECTS__*/ (args) => { + // ... + const b = console.log('BBB') + console.log(b) +} + +/*#__NO_SIDE_EFFECTS__*/ +const fnC = (args) => { + // ... + + const c = console.log('CCC') + console.log(c) +} + + +/** + * Some jsdocs + * + * @__NO_SIDE_EFFECTS__ + */ +const fnD = (args) => { + // ... + const d = console.log('DDD') + console.log(d) +} + + +fnA() +fnA() +fnA() +fnA() +fnA() +fnA() +fnA() +fnA() +fnA() +fnA() + +fnB() +fnB() +fnB() +fnB() +fnB() +fnB() +fnB() +fnB() +fnB() +fnB() + +fnC() +fnC() +fnC() +fnC() +fnC() +fnC() +fnC() +fnC() +fnC() +fnC() + +fnD() +fnD() +fnD() +fnD() +fnD() +fnD() +fnD() +fnD() +fnD() +fnD() \ No newline at end of file diff --git a/crates/swc_ecma_minifier/tests/full/size/b44e25a2b8c64cd1d2a448bc214c8f9a589b1245/output.js b/crates/swc_ecma_minifier/tests/full/size/b44e25a2b8c64cd1d2a448bc214c8f9a589b1245/output.js index 584545f1bf20..b330686e4c5f 100644 --- a/crates/swc_ecma_minifier/tests/full/size/b44e25a2b8c64cd1d2a448bc214c8f9a589b1245/output.js +++ b/crates/swc_ecma_minifier/tests/full/size/b44e25a2b8c64cd1d2a448bc214c8f9a589b1245/output.js @@ -1 +1 @@ -self=self||[].push[{8:function(){0()}}]; +self=self||[].push[{8:function(){}}]; diff --git a/crates/swc_ecma_minifier/tests/full/size/de4c599f0856587c5478f4f8d3cce9d91f9c8937/output.js b/crates/swc_ecma_minifier/tests/full/size/de4c599f0856587c5478f4f8d3cce9d91f9c8937/output.js index 7b6e35f3216f..72453783dcfa 100644 --- a/crates/swc_ecma_minifier/tests/full/size/de4c599f0856587c5478f4f8d3cce9d91f9c8937/output.js +++ b/crates/swc_ecma_minifier/tests/full/size/de4c599f0856587c5478f4f8d3cce9d91f9c8937/output.js @@ -1 +1 @@ -self=self||[].push[{4:function(){0()},80288:0}]; +self=self||[].push[{4:function(){},80288:0}];