Repository: swc-project/swc (rust)

Issue:
Tagged template literals: minify does not shake pure tag function
### Describe the bug

Unused tagged template literals are not tree shaken away from the minified output when the tag function is marked as pure using the `/*#__PURE__*/` directive and/or by `jsc.minify.compress.pure_funcs` configuration.

I'm not sure if this would be considered a bug or a feature request.

### Input code

```typescript
/*#__PURE__*/
function pure_html(strings, ...values) {
  return { strings, values };
}

// ✅ This is tree-shaken from the minified output.
const a = `<span>aaaa</span>`;

// ❌ This is not tree-shaken from the minified output,
// despite the "pure_html" function being declared pure
// using the __PURE__ directive and by 
// `jsc.minify.compress.pure_funcs` configuration.
const b = pure_html`<span>bbbb</span>`;

// ✅ This is not tree-shaken from the minified output.
export const c  = pure_html`<span>cccc</span>`;
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es2022",
    "loose": false,
    "minify": {
      "compress": {
        "pure_funcs": ["pure_html"]
      },
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": true,
  "isModule": true
}
```


### Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.3.94-nightly-20231016.1&code=H4sIAAAAAAAAA42QQU7DMBBF9z7FV9lAVZwDtLBjjxCsE8eZNIbGjjx2RVV1yY4jwOU4CXZKI4RYdOSFNTN%2Fvt4v5hdlef%2F0cFeW80K00epgnMUQPZVd6DeXHLyxa15ASrlVm0h8hb0APIXoLfaYFo5THJbiIERR4OvjDY%2BdYaQXPNE1d%2BqFLFrveoSO0BtrWkMNXAxDDFJoZzlA4QbVigdlb1WqVTF%2Bq%2BXx6Of7dNS6cNbhRRY2xIMJNM5nmWyGibamRJA29Eb5pMrwWRI5t7PgFBEa4ylptgRlG9Q75L3qmbUcPXdSu37wxCzHBLMDV0hcrVlHr7LbCbNOmFPMP7x1qj%2B8v0I8l1cKeh2cDzgaafzjpFNNTt%2F37f80BQIAAA%3D%3D&config=H4sIAAAAAAAAA1WPTQ7CIBBG9z1FM2sXpgsX3sETGGMITisNf2EgkTS9u0Ch1h3zvvnCm6Xre5iJw7Vf0jMNljlCt8%2BJUNSefRIB5IoRd8J6OLV0phyNTBIWtG4JeOYm9KVFw3kYagOkMYStUZkSWozx%2BCc3yjokOrDsFhw%2Bx6B55nd4eyXhUeN1N1JMTxL%2Fpbq6AMq8QgnruT5a3CQv8FtqPt6F4giCbq2YWbd%2BAdH3Ab05AQAA

### SWC Info output

_No response_

### Expected behavior

When a template tag function is marked "pure", minification should take it into consideration when considering whether an expression has side-effects.

### Actual behavior

_No response_

### Version

1.3.94-nightly-20231016.1

### Additional context

_No response_

Pull request description:
fix(es/minifier): Respect `pure_funcs` for tagged tpls
**Related issue:**

 - Closes #8275

Patch:
diff --git a/crates/swc_ecma_minifier/src/compress/pure/misc.rs b/crates/swc_ecma_minifier/src/compress/pure/misc.rs
index 7e41aa1b65..1eee79a6f9 100644
--- a/crates/swc_ecma_minifier/src/compress/pure/misc.rs
+++ b/crates/swc_ecma_minifier/src/compress/pure/misc.rs
@@ -928,20 +928,35 @@ impl Pure<'_> {
             _ => {}
         }
 
-        if let Expr::Call(CallExpr {
-            callee: Callee::Expr(callee),
-            args,
-            ..
-        }) = e
-        {
-            if callee.is_pure_callee(&self.expr_ctx) {
-                self.changed = true;
-                report_change!("Dropping pure call as callee is pure");
-                *e = self
-                    .make_ignored_expr(args.take().into_iter().map(|arg| arg.expr))
-                    .unwrap_or(Expr::Invalid(Invalid { span: DUMMY_SP }));
-                return;
+        match e {
+            Expr::Call(CallExpr {
+                callee: Callee::Expr(callee),
+                args,
+                ..
+            }) => {
+                if callee.is_pure_callee(&self.expr_ctx) {
+                    self.changed = true;
+                    report_change!("Dropping pure call as callee is pure");
+                    *e = self
+                        .make_ignored_expr(args.take().into_iter().map(|arg| arg.expr))
+                        .unwrap_or(Expr::Invalid(Invalid { span: DUMMY_SP }));
+                    return;
+                }
+            }
+
+            Expr::TaggedTpl(TaggedTpl {
+                tag: callee, tpl, ..
+            }) => {
+                if callee.is_pure_callee(&self.expr_ctx) {
+                    self.changed = true;
+                    report_change!("Dropping pure tag tpl as callee is pure");
+                    *e = self
+                        .make_ignored_expr(tpl.exprs.take().into_iter())
+                        .unwrap_or(Expr::Invalid(Invalid { span: DUMMY_SP }));
+                    return;
+                }
             }
+            _ => (),
         }
 
         if self.options.unused {
diff --git a/crates/swc_ecma_minifier/src/metadata/mod.rs b/crates/swc_ecma_minifier/src/metadata/mod.rs
index ee353458e3..3d564b95da 100644
--- a/crates/swc_ecma_minifier/src/metadata/mod.rs
+++ b/crates/swc_ecma_minifier/src/metadata/mod.rs
@@ -58,6 +58,39 @@ struct InfoMarker<'a> {
     state: State,
 }
 
+impl InfoMarker<'_> {
+    fn is_pure_callee(&self, callee: &Expr) -> bool {
+        match callee {
+            Expr::Ident(callee) => {
+                if self.pure_callee.contains(&callee.to_id()) {
+                    return true;
+                }
+            }
+
+            Expr::Seq(callee) => {
+                if has_pure(self.comments, callee.span) {
+                    return true;
+                }
+            }
+            _ => (),
+        }
+
+        if let Some(pure_fns) = &self.pure_funcs {
+            if let Expr::Ident(..) = callee {
+                // Check for pure_funcs
+                if Ident::within_ignored_ctxt(|| {
+                    //
+                    pure_fns.contains(&NodeIgnoringSpan::borrowed(callee))
+                }) {
+                    return true;
+                }
+            }
+        }
+
+        has_pure(self.comments, callee.span())
+    }
+}
+
 impl VisitMut for InfoMarker<'_> {
     noop_visit_mut_type!();
 
@@ -71,19 +104,9 @@ impl VisitMut for InfoMarker<'_> {
         // We check callee in some cases because we move comments
         // See https://github.com/swc-project/swc/issues/7241
         if match &n.callee {
-            Callee::Expr(e) => match &**e {
-                Expr::Ident(callee) => self.pure_callee.contains(&callee.to_id()),
-                _ => false,
-            },
+            Callee::Expr(e) => self.is_pure_callee(e),
             _ => false,
         } || has_pure(self.comments, n.span)
-            || match &n.callee {
-                Callee::Expr(e) => match &**e {
-                    Expr::Seq(callee) => has_pure(self.comments, callee.span),
-                    _ => false,
-                },
-                _ => false,
-            }
         {
             if !n.span.is_dummy_ignoring_cmt() {
                 n.span = n.span.apply_mark(self.marks.pure);
@@ -185,6 +208,16 @@ impl VisitMut for InfoMarker<'_> {
         }
     }
 
+    fn visit_mut_tagged_tpl(&mut self, n: &mut TaggedTpl) {
+        n.visit_mut_children_with(self);
+
+        if has_pure(self.comments, n.span) || self.is_pure_callee(&n.tag) {
+            if !n.span.is_dummy_ignoring_cmt() {
+                n.span = n.span.apply_mark(self.marks.pure);
+            }
+        }
+    }
+
     fn visit_mut_var_decl(&mut self, n: &mut VarDecl) {
         n.visit_mut_children_with(self);
 


Test patch:
diff --git a/crates/swc/tests/fixture/issues-8xxx/8275/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8275/input/.swcrc
new file mode 100644
index 0000000000..37796ffcba
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8275/input/.swcrc
@@ -0,0 +1,22 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "ecmascript",
+            "jsx": false
+        },
+        "target": "es2022",
+        "loose": false,
+        "minify": {
+            "compress": {
+                "pure_funcs": [
+                    "pure_html"
+                ]
+            },
+            "mangle": false
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8275/input/1.js b/crates/swc/tests/fixture/issues-8xxx/8275/input/1.js
new file mode 100644
index 0000000000..9300d99496
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8275/input/1.js
@@ -0,0 +1,15 @@
+/*#__PURE__*/
+function pure_html(strings, ...values) {
+    return { strings, values };
+}
+
+// ✅ This is tree-shaken from the minified output.
+const a = `<span>aaaa</span>`;
+
+// ❌ This is not tree-shaken from the minified output,
+// despite the "html" function being declare using
+// `jsc.minify.compress.pure_funcs` configuration.
+const b = pure_html`<span>bbbb</span>`;
+
+// ✅ This is not tree-shaken from the minified output.
+export const c = pure_html`<span>cccc</span>`;
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8275/output/1.js b/crates/swc/tests/fixture/issues-8xxx/8275/output/1.js
new file mode 100644
index 0000000000..41a9c9ad50
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8275/output/1.js
@@ -0,0 +1,6 @@
+export const c = (function(strings, ...values) {
+    return {
+        strings,
+        values
+    };
+})`<span>cccc</span>`;