Repository: swc-project/swc (rust)

Issue:
`dead_branch_remover` hoists variable out of function expressions
### Describe the bug

The `dead_branch_remover` hoists a variable out of a function expression in a dead branch, and then also doesn't remove the variable even though it was a dead branch.

### Input code

```typescript
if (false) {
    foo(function () {
        var module = {};
        return module;
    });
}
```


### Config

```rust
use std::collections::HashSet;

use swc::{self};
use swc_atoms::JsWord;
use swc_common::{
    chain,
    comments::SingleThreadedComments,
    errors::{DiagnosticBuilder, Emitter, Handler},
    sync::Lrc,
    FileName, Globals, Mark, SourceMap, SyntaxContext, DUMMY_SP,
};
use swc_ecma_ast::{CatchClause, Decl, Id, Ident, Module, Pat, Stmt, VarDeclKind};
use swc_ecma_codegen::{text_writer::JsWriter, Config};
use swc_ecma_parser::{lexer::Lexer, EsConfig, PResult, Parser, StringInput, Syntax, TsConfig};
use swc_ecma_preset_env::{preset_env, BrowserData, Mode, Targets, Version};
use swc_ecma_transforms::{
    compat::reserved_words::reserved_words,
    fixer,
    fixer::paren_remover,
    helpers, hygiene,
    optimization::simplify::{dead_branch_remover, expr_simplifier},
    react::{self, Runtime},
    resolver,
};
use swc_ecma_visit::{Fold, FoldWith, Visit, VisitWith};

#[derive(Debug, Clone, Default)]
pub struct ErrorBuffer(std::sync::Arc<std::sync::Mutex<Vec<swc_common::errors::Diagnostic>>>);

impl Emitter for ErrorBuffer {
    fn emit(&mut self, db: &DiagnosticBuilder) {
        self.0.lock().unwrap().push((**db).clone());
    }
}

fn main() {
    let cm = Lrc::<SourceMap>::default();

    let src = r#"
if (false) {
    foo(function () {
        var module = {};
        return module;
    });
}
"#;
    let (module, comments) = parse(src, "test.js", &cm).unwrap();

    let error_buffer = ErrorBuffer::default();
    let handler = Handler::with_emitter(true, false, Box::new(error_buffer.clone()));
    swc_common::errors::HANDLER.set(&handler, || {
        swc_common::GLOBALS.set(&Globals::new(), || {
            helpers::HELPERS.set(&helpers::Helpers::new(true), || {
                let unresolved_mark = Mark::fresh(Mark::root());
                let top_level_mark = Mark::fresh(Mark::root());
                let module =
                    module.fold_with(&mut resolver(unresolved_mark, top_level_mark, false));

                let module = module.fold_with(&mut dead_branch_remover(unresolved_mark));

                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()
        }),
        // Syntax::Typescript(TsConfig {
        //     tsx: 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,
            ..Default::default()
        };
        let mut emitter = swc_ecma_codegen::Emitter {
            cfg: config,
            comments: Some(&comments),
            cm,
            wr: writer,
        };
        emitter.emit_module(&module).unwrap();
    }

    String::from_utf8(buf).unwrap()
}
```


### Playground link

_No response_

### Expected behavior

Everything is removed

### Actual behavior

Output is
```js
var module;
```

### Version

0a1e30a4f850949ce2ec5b7e95f53cb1a3d4b771

### Additional context

https://github.com/parcel-bundler/parcel/issues/7882

Pull request description:
fix(es/transforms_optimization): should capture hoisted variables correctly
<!-- Note: CI script will automatically rebase your PR so please do not rebase unless required -->

<!-- Thanks for submitting a pull request! Please provide enough information so that others can review your pull request. -->

**Description:**

<!-- Explain the **motivation** for making this change. What existing problem does the pull request solve? -->

**BREAKING CHANGE:**

<!--
If this PR introduces a breaking change, it must contain a notice for it to be included in the CHANGELOG. Add description or remove entirely if not breaking.

You may need to update `MIGRATION.md` for the breaking changes.
-->

**Related issue (if exists):**
Fix #6732.


Patch:
diff --git a/crates/swc_ecma_utils/src/lib.rs b/crates/swc_ecma_utils/src/lib.rs
index c11d36ebb5..e98cf48895 100644
--- a/crates/swc_ecma_utils/src/lib.rs
+++ b/crates/swc_ecma_utils/src/lib.rs
@@ -494,8 +494,6 @@ impl Visit for Hoister {
 
     fn visit_fn_decl(&mut self, f: &FnDecl) {
         self.vars.push(f.ident.clone());
-
-        f.visit_children_with(self)
     }
 
     fn visit_pat(&mut self, p: &Pat) {
@@ -513,6 +511,8 @@ impl Visit for Hoister {
 
         v.visit_children_with(self)
     }
+
+    fn visit_fn_expr(&mut self, _n: &FnExpr) {}
 }
 
 #[derive(Debug, Clone)]


Test patch:
diff --git a/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn/output.js b/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn/output.js
index e17475289f..6250b8b91e 100644
--- a/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn/output.js
+++ b/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn/output.js
@@ -1,5 +1,5 @@
 function f() {
-    var x, g, q, y;
+    var x, g;
     g();
     x = 10;
     throw Error("foo");
diff --git a/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn_strict/output.js b/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn_strict/output.js
index 275bbb11bc..dc2a7837ae 100644
--- a/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn_strict/output.js
+++ b/crates/swc_ecma_minifier/tests/terser/compress/dead_code/dead_code_2_should_warn_strict/output.js
@@ -1,6 +1,6 @@
 "use strict";
 function f() {
-    var x, g, q, y;
+    var x, g;
     g();
     x = 10;
     throw Error("foo");
diff --git a/crates/swc_ecma_transforms_optimization/src/simplify/branch/tests.rs b/crates/swc_ecma_transforms_optimization/src/simplify/branch/tests.rs
index ef2905a369..f6cf3aeef9 100644
--- a/crates/swc_ecma_transforms_optimization/src/simplify/branch/tests.rs
+++ b/crates/swc_ecma_transforms_optimization/src/simplify/branch/tests.rs
@@ -1815,3 +1815,33 @@ fn issue_1825() {
 fn issue_1851_1() {
     test("x ?? (x = 'abc');", "x ?? (x = 'abc');");
 }
+
+#[test]
+fn issue_6732_1() {
+    test(
+        "
+    if (false) {
+        foo(function () {
+            var module = {};
+            return module;
+        });
+    }
+    ",
+        "",
+    );
+}
+
+#[test]
+fn issue_6732_2() {
+    test(
+        "
+    if (false) {
+        function foo() {
+            var module = {};
+            return module;
+        };
+    }
+    ",
+        "var foo;",
+    );
+}