Repository: swc-project/swc (rust)

Issue:
`expr_simplifier` removes pure comment around IIFE
### Describe the bug

`expr_simplifier` removes the `ParenExpr` around a IIFE but also removes a `#__PURE__` comment in the process.

So in that sense, `expr_simplifier` isn't "safe".

<details>
<summary>Full running example</summary>

```rust
use swc_common::{
    chain, comments::SingleThreadedComments, sync::Lrc, FileName, Globals, Mark, SourceMap,
};
use swc_ecma_ast::Module;
use swc_ecma_codegen::{text_writer::JsWriter, Config, Emitter};
use swc_ecma_parser::{lexer::Lexer, EsConfig, PResult, Parser, StringInput, Syntax};
use swc_ecma_transforms::{
    compat::reserved_words::reserved_words, fixer, helpers, hygiene,
    optimization::simplify::expr_simplifier, resolver::resolver_with_mark,
};
use swc_ecma_visit::FoldWith;

fn main() {
    let cm = Lrc::<SourceMap>::default();
    let src = r#"
var flm = /*#__PURE__*/ hMap(flt, 9, 0);
var deo = /*#__PURE__*/ new Uint32Array([
    65540, 131080, 131088
]);
var Unzip = /*#__PURE__*/ (function () {
    console.log("Unzip")
}());
"#;
    let (module, comments) = parse(src, "test.js", &cm).unwrap();

    swc_common::GLOBALS.set(&Globals::new(), || {
        helpers::HELPERS.set(&helpers::Helpers::default(), || {
            let global_mark = Mark::fresh(Mark::root());
            let module = module.fold_with(&mut resolver_with_mark(global_mark));

            let transform = &mut expr_simplifier(Default::default());
            let module = module.fold_with(transform);
            let module = module.fold_with(&mut chain!(
                reserved_words(),
                hygiene(),
                fixer(Some(&comments))
            ));

            let code = emit(&module, &comments, cm);
            println!("{}", code);
        });
    });
}

fn parse(
    code: &str,
    filename: &str,
    cm: &Lrc<SourceMap>,
) -> PResult<(Module, SingleThreadedComments)> {
    let source_file = cm.new_source_file(FileName::Real(filename.into()), code.into());
    let comments = SingleThreadedComments::default();

    let lexer = Lexer::new(
        Syntax::Es(EsConfig {
            jsx: true,
            ..Default::default()
        }),
        Default::default(),
        StringInput::from(&*source_file),
        Some(&comments),
    );
    let mut parser = Parser::new_from(lexer);
    match parser.parse_module() {
        Err(err) => Err(err),
        Ok(module) => Ok((module, comments)),
    }
}

fn emit(module: &Module, comments: &SingleThreadedComments, cm: Lrc<SourceMap>) -> String {
    let mut buf = vec![];
    {
        let writer = Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None));
        let config = Config { minify: false };
        let mut emitter = Emitter {
            cfg: config,
            comments: Some(&comments),
            cm,
            wr: writer,
        };
        emitter.emit_module(&module).unwrap();
    }

    String::from_utf8(buf).unwrap()
}
```

</details>

### Input code

```typescript
var flm = /*#__PURE__*/ hMap(flt, 9, 0);
var deo = /*#__PURE__*/ new Uint32Array([
    65540, 131080, 131088
]);
var Unzip = /*#__PURE__*/ (function () {
    console.log("Unzip")
}());
```


### Config

_No response_

### Playground link

_No response_

### Expected behavior

Same output as input

### Actual behavior

```js
var flm = /*#__PURE__*/ hMap(flt, 9, 0);
var deo = /*#__PURE__*/ new Uint32Array([
    65540,
    131080,
    131088
]);
var Unzip = function() { // <--- missing PURE
    console.log("Unzip");
}();
```

### Version

cb603cc1ffcf9693ccbc657ee111322f0e41312e

### Additional context

https://github.com/parcel-bundler/parcel/issues/7481

Pull request description:
feat(es/transforms/base): Add `paren_remover`
**Description:**

This pass should be applied as the first pass.

**Related issue:**

 - Closes #3115

Patch:
diff --git a/crates/swc_ecma_transforms_base/src/fixer.rs b/crates/swc_ecma_transforms_base/src/fixer.rs
index 50cf6ae467..626d481678 100644
--- a/crates/swc_ecma_transforms_base/src/fixer.rs
+++ b/crates/swc_ecma_transforms_base/src/fixer.rs
@@ -14,6 +14,17 @@ pub fn fixer(comments: Option<&dyn Comments>) -> impl '_ + Fold + VisitMut {
         ctx: Default::default(),
         span_map: Default::default(),
         in_for_stmt_head: Default::default(),
+        remove_only: false,
+    })
+}
+
+pub fn paren_remover(comments: Option<&dyn Comments>) -> impl '_ + Fold + VisitMut {
+    as_folder(Fixer {
+        comments,
+        ctx: Default::default(),
+        span_map: Default::default(),
+        in_for_stmt_head: Default::default(),
+        remove_only: true,
     })
 }
 
@@ -27,6 +38,8 @@ struct Fixer<'a> {
     span_map: AHashMap<Span, Span>,
 
     in_for_stmt_head: bool,
+
+    remove_only: bool,
 }
 
 #[repr(u8)]
@@ -801,6 +814,10 @@ impl Fixer<'_> {
 
     /// Wrap with a paren.
     fn wrap(&mut self, e: &mut Expr) {
+        if self.remove_only {
+            return;
+        }
+
         let span = e.span();
 
         let span = if let Some(span) = self.span_map.remove(&span) {


Test patch:
diff --git a/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/input.js b/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/input.js
new file mode 100644
index 0000000000..79e200676f
--- /dev/null
+++ b/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/input.js
@@ -0,0 +1,7 @@
+var flm = /*#__PURE__*/ hMap(flt, 9, 0);
+var deo = /*#__PURE__*/ new Uint32Array([
+    65540, 131080, 131088
+]);
+var Unzip = /*#__PURE__*/ (function () {
+    console.log("Unzip")
+}());
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/output.js b/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/output.js
new file mode 100644
index 0000000000..f76779d007
--- /dev/null
+++ b/crates/swc_ecma_transforms_optimization/tests/expr-simplifier/issue-3115/output.js
@@ -0,0 +1,9 @@
+var flm = /*#__PURE__*/ hMap(flt, 9, 0);
+var deo = /*#__PURE__*/ new Uint32Array([
+    65540,
+    131080,
+    131088
+]);
+var Unzip = /*#__PURE__*/ function() {
+    console.log("Unzip");
+}();
diff --git a/crates/swc_ecma_transforms_optimization/tests/fixture.rs b/crates/swc_ecma_transforms_optimization/tests/fixture.rs
index 5c13566f71..9822750615 100644
--- a/crates/swc_ecma_transforms_optimization/tests/fixture.rs
+++ b/crates/swc_ecma_transforms_optimization/tests/fixture.rs
@@ -1,9 +1,17 @@
 use std::path::PathBuf;
 
-use swc_common::pass::Repeat;
+use swc_common::{chain, pass::Repeat};
 use swc_ecma_parser::{EsConfig, Syntax};
+use swc_ecma_transforms_base::fixer::paren_remover;
 use swc_ecma_transforms_optimization::simplify::{dce::dce, expr_simplifier};
-use swc_ecma_transforms_testing::test_fixture;
+use swc_ecma_transforms_testing::{test_fixture, Tester};
+use swc_ecma_visit::{as_folder, Fold, VisitMut};
+
+fn remover(t: &Tester) -> impl VisitMut + Fold {
+    as_folder(paren_remover(Some(
+        Box::leak(Box::new(t.comments.clone())) as _
+    )))
+}
 
 #[testing::fixture("tests/dce/**/input.js")]
 fn dce_single_pass(input: PathBuf) {
@@ -14,7 +22,7 @@ fn dce_single_pass(input: PathBuf) {
             decorators: true,
             ..Default::default()
         }),
-        &|_| dce(Default::default()),
+        &|t| chain!(remover(t), dce(Default::default())),
         &input,
         &output,
     );
@@ -29,7 +37,7 @@ fn dce_repeated(input: PathBuf) {
             decorators: true,
             ..Default::default()
         }),
-        &|_| Repeat::new(dce(Default::default())),
+        &|t| chain!(remover(t), Repeat::new(dce(Default::default()))),
         &input,
         &output,
     );
@@ -43,7 +51,7 @@ fn expr(input: PathBuf) {
         Syntax::Es(EsConfig {
             ..Default::default()
         }),
-        &|_| Repeat::new(expr_simplifier(Default::default())),
+        &|t| chain!(remover(t), Repeat::new(expr_simplifier(Default::default()))),
         &input,
         &output,
     );