Repository: swc-project/swc (rust)

Issue:
Incorrect `for(let` transpilation with nested for-loops without block statement bodies
### Describe the bug

When running `dead_branch_remover` before `preset_env`, nested loops with let bindings are transpiled incorrectly.

### Input code

```js
let funcs = [];
for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
        funcs.push(() => console.log(i, j));
    }
}

funcs.forEach((f) => f());
```

Complete reproduction:

```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#"
let funcs = [];
for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
        funcs.push(() => console.log(i, j));
    }
}

funcs.forEach((f) => f());
"#;
    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!(
                    preset_env(
                        top_level_mark,
                        Some(&comments),
                        swc_ecma_preset_env::Config {
                            targets: Some(Targets::Versions(BrowserData {
                                safari: Some(Version {
                                    major: 9,
                                    minor: 0,
                                    patch: 0,
                                }),
                                ..Default::default()
                            })),
                            bugfixes: true,
                            shipped_proposals: true,
                            mode: Some(Mode::Entry),
                            ..Default::default()
                        },
                        Default::default(),
                        &mut Default::default()
                    ),
                    helpers::inject_helpers(),
                ));

                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()
}
```


### Config

_No response_

### Playground link

_No response_

### Expected behavior

Correct transpilation (also what happens without `dead_branch_remover`)
```js
var _loop = function(i) {
    var _loop = function(j) {
        funcs.push(function() {
            return console.log(i, j);
        });
    };
    for(var j = 0; j < 2; j++)_loop(j);
};
var funcs = [];
for(var i = 0; i < 2; i++)_loop(i);
funcs.forEach(function(f) {
    return f();
});
```

### Actual behavior

```js
var _loop = function(j) {
    funcs.push(function() {
        return console.log(i, j); // i is treated as a global variable
    });
};
var funcs = [];
for(var i = 0; i < 2; i++)for(var j = 0; j < 2; j++)_loop(j);
funcs.forEach(function(f) {
    return f();
});
```

### Version

8bee06f78f403b43e5da2126297b9aac258a95d6

### Additional context

https://github.com/parcel-bundler/parcel/issues/8670

Pull request description:
fix(es/transform_compat): should prepend generated `_loop` function to correct scope
<!-- 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 #6573

Patch:
diff --git a/crates/swc_ecma_transforms_compat/src/es2015/block_scoping/mod.rs b/crates/swc_ecma_transforms_compat/src/es2015/block_scoping/mod.rs
index ad41644c46..361ac3166b 100644
--- a/crates/swc_ecma_transforms_compat/src/es2015/block_scoping/mod.rs
+++ b/crates/swc_ecma_transforms_compat/src/es2015/block_scoping/mod.rs
@@ -361,6 +361,44 @@ impl BlockScoping {
             *body = Box::new(call.take().into_stmt());
         }
     }
+
+    /// This method will turn stmt like
+    /// ```js
+    /// for (let i in [1, 2])
+    ///   for (let j in [1, 2])
+    ///     console.log(i, j)
+    /// ```
+    /// into
+    /// ```js
+    /// for (let i in [1, 2]) {
+    ///   for (let j in [1, 2]) {
+    ///     console.log(i, j)
+    ///   }
+    /// }
+    /// ```
+    /// which fixes https://github.com/swc-project/swc/issues/6573
+    fn blockify_for_stmt_body(&self, body: &mut Box<Stmt>) -> bool {
+        if !body.is_block() {
+            *body = Box::new(Stmt::Block(BlockStmt {
+                span: Default::default(),
+                stmts: vec![*body.take()],
+            }));
+            true
+        } else {
+            false
+        }
+    }
+
+    fn undo_blockify_for_stmt_body(&self, body: &mut Box<Stmt>, blockifyed: bool) {
+        if blockifyed {
+            let stmt = body
+                .as_mut_block()
+                .and_then(|block| (block.stmts.len() == 1).then(|| block.stmts[0].take()));
+            if let Some(stmt) = stmt {
+                *body = Box::new(stmt)
+            }
+        }
+    }
 }
 
 #[swc_trace]
@@ -400,6 +438,7 @@ impl VisitMut for BlockScoping {
     }
 
     fn visit_mut_for_in_stmt(&mut self, node: &mut ForInStmt) {
+        let blockifyed = self.blockify_for_stmt_body(&mut node.body);
         let lexical_var = if let VarDeclOrPat::VarDecl(decl) = &node.left {
             find_lexical_vars(decl)
         } else {
@@ -420,9 +459,11 @@ impl VisitMut for BlockScoping {
 
         self.visit_mut_with_scope(kind, &mut node.body);
         self.handle_capture_of_vars(&mut node.body);
+        self.undo_blockify_for_stmt_body(&mut node.body, blockifyed);
     }
 
     fn visit_mut_for_of_stmt(&mut self, node: &mut ForOfStmt) {
+        let blockifyed = self.blockify_for_stmt_body(&mut node.body);
         let vars = if let VarDeclOrPat::VarDecl(decl) = &node.left {
             find_lexical_vars(decl)
         } else {
@@ -444,9 +485,11 @@ impl VisitMut for BlockScoping {
 
         self.visit_mut_with_scope(kind, &mut node.body);
         self.handle_capture_of_vars(&mut node.body);
+        self.undo_blockify_for_stmt_body(&mut node.body, blockifyed);
     }
 
     fn visit_mut_for_stmt(&mut self, node: &mut ForStmt) {
+        let blockifyed = self.blockify_for_stmt_body(&mut node.body);
         let lexical_var = if let Some(VarDeclOrExpr::VarDecl(decl)) = &node.init {
             find_lexical_vars(decl)
         } else {
@@ -467,6 +510,7 @@ impl VisitMut for BlockScoping {
         };
         self.visit_mut_with_scope(kind, &mut node.body);
         self.handle_capture_of_vars(&mut node.body);
+        self.undo_blockify_for_stmt_body(&mut node.body, blockifyed);
     }
 
     fn visit_mut_function(&mut self, f: &mut Function) {


Test patch:
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/input.js
new file mode 100644
index 0000000000..74dc864031
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/input.js
@@ -0,0 +1,6 @@
+let funcs = [];
+for (let i = 0; i < 2; i++)
+    for (let j = 0; j < 2; j++)
+        funcs.push(() => console.log(i, j));
+
+funcs.forEach((f) => f());
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/output.js
new file mode 100644
index 0000000000..511bc63ffe
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/1/output.js
@@ -0,0 +1,9 @@
+var _loop__7 = function(i__2) {
+    var _loop__6 = function(j__3) {
+        funcs__1.push(()=>console.log(i__2, j__3));
+    };
+    for(var j__3 = 0; j__3 < 2; j__3++)_loop__6(j__3);
+};
+var funcs__1 = [];
+for(var i__2 = 0; i__2 < 2; i__2++)_loop__7(i__2);
+funcs__1.forEach((f__5)=>f__5());
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/input.js
new file mode 100644
index 0000000000..6d47cdd17c
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/input.js
@@ -0,0 +1,6 @@
+let funcs = [];
+for (let i in [1, 2])
+    for (let j in [1, 2])
+        funcs.push(() => console.log(i, j));
+
+funcs.forEach((f) => f());
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/output.js
new file mode 100644
index 0000000000..88e919705d
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/2/output.js
@@ -0,0 +1,15 @@
+var _loop__7 = function(i__2) {
+    var _loop__6 = function(j__3) {
+        funcs__1.push(()=>console.log(i__2, j__3));
+    };
+    for(var j__3 in [
+        1,
+        2
+    ])_loop__6(j__3);
+};
+var funcs__1 = [];
+for(var i__2 in [
+    1,
+    2
+])_loop__7(i__2);
+funcs__1.forEach((f__5)=>f__5());
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/input.js
new file mode 100644
index 0000000000..4412a98c95
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/input.js
@@ -0,0 +1,6 @@
+let funcs = [];
+for (let i of [1, 2])
+    for (let j of [1, 2])
+        funcs.push(() => console.log(i, j));
+
+funcs.forEach((f) => f());
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/output.js
new file mode 100644
index 0000000000..c3f92be2ff
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-6573/3/output.js
@@ -0,0 +1,15 @@
+var _loop__7 = function(i__2) {
+    var _loop__6 = function(j__3) {
+        funcs__1.push(()=>console.log(i__2, j__3));
+    };
+    for (var j__3 of [
+        1,
+        2
+    ])_loop__6(j__3);
+};
+var funcs__1 = [];
+for (var i__2 of [
+    1,
+    2
+])_loop__7(i__2);
+funcs__1.forEach((f__5)=>f__5());