Repository: swc-project/swc (rust)

Issue:
Support __NO_SIDE_EFFECTS__ in the minifier
### Describe the feature

Rollup and esbuild now have support for `/*#__NO_SIDE_EFFECTS__*/` comments (which is similar to `/*#__PURE__*/` but instead on the function declaration)

https://github.com/rollup/rollup/pull/5024
https://github.com/evanw/esbuild/issues/3149
https://github.com/terser/terser/issues/1130

The swc minifier could also use this information

### Babel plugin or link to the feature description

_No response_

### Additional context

_No response_

Pull request:
feat(es/minifier): Support `__NO_SIDE_EFFECTS__`
**Description:**

**Related issue:**

 - Closes #7525.

Patch:
diff --git a/crates/swc_ecma_minifier/src/metadata/mod.rs b/crates/swc_ecma_minifier/src/metadata/mod.rs
index de4541a511..c6a5636239 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<FxHashSet<NodeIgnoringSpan<'a, Expr>>>,
+    pure_callee: FxHashSet<Id>,
+
     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<F>(&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<Id>,
-}
+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<Id>,
 }
 
-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<Id> = 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>(n: &mut N, unresolved_mark: Mark) -> bool
-// where
-//     N: VisitMutWith<IdentCollector>,
-// {
-//     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<Id>,
-    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<F>(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
+    })
 }


Test patch:
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 0000000000..ac4e89404c
--- /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 0000000000..7fbe8a1f73
--- /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 584545f1bf..b330686e4c 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 +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 7b6e35f321..72453783dc 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 +1,1 @@
-self=self||[].push[{4:function(){0()},80288:0}];
+self=self||[].push[{4:function(){},80288:0}];