Repository: swc-project/swc (rust)

Issue:
`break` and `continue` statements not executing due to incorrect `for` loop body extraction with `switch` statement when target is `es5`
### Describe the bug

Loop body is extracted into `_loop` function and any `break` or `continue` statements are rewritten to instead `return` `'break'` or `'continue'` string. Returned value from the extracted loop body is used in `switch` clause to `break` or `continue` the loop execution, but it instead `breaks` or `continues` the `switch` clause.

Reproduction code seems to be when the loop has both `break` and `continue` statements and when a function inside the loop references the loop variable (`i`). When any of these conditions are not met code compiles correctly.

Targets higher than `es5` do not cause this issue.

Babel had similar [issue](https://github.com/babel/babel/issues/11800) with the same broken `switch` clause.

### Input code

```typescript
for (let i = 0; i < 2; i++) {
  
  if (i === 0) continue
  if (i === 1) break

  [1].forEach((_) => {
    console.log(i)
  })

}
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es5",
    "loose": false,
    "minify": {
      "compress": false,
      "mangle": false
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link

https://play.swc.rs/?version=1.2.157&code=H4sIAAAAAAAAA0vLL1LQyEktUchUsFUwsAZSNgpGQEpbW1OhmpdLQQGEM9MUNIDytkAVmgrJ%2BXklmXmlqagShpoKSUWpidm8XCDxaMNYvbT8ItfE5AwNjXhNBVs7iGEKIN3F%2BTmpejn56RqZmiCxWk2QnloAfi%2Br6okAAAA%3D&config=H4sIAAAAAAAAA0WMTQrEIAxG75K1286id5hDBCctFv9IUhgR714tlu7C915ehUMsrBUyshCPS0pU%2FMMKZAOKZZcVTNf6tKEXagYUeScdiiyd%2BZSEJjUQXHRbGSWbQmYSeRHG3T9m66GQfucYKmjJdAc%2F0N7G%2FHPynaLySe0Cj9ke9LUAAAA%3D

### Expected behavior

`break` and `continue` statements correctly `break` or `continue` the `for` loop instead of affecting the `switch` clause.

### Actual behavior

Loop body is extracted into `_loop` function and then executed with
```javascript
for(var i = 0; i < 2; i++){
    var _ret = _loop(i);
    switch(_ret){
        case "break":
            break;
        case "continue":
            continue;
    }
}
```
which will not `break` or `continue` the `for` loop but instead will `break` or `continue` the `switch` clause.

### Version

1.2.157

### Additional context

I encountered this issue when upgrading `Next.js` to `12.0.9`. Versions up to `12.1.2` also have this issue. Reverting from `swc` to `babel` compilation (by adding `babel.config.js` file to project directory) or downgrading `Next.js` to `12.0.8` solves the issue.

Pull request description:
fix(es/compat): remove `continue` in block scoping
<!-- 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:**
It seems like `continue` is not needed in this scenario, tsc has a better output.
<!-- 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):**
closes #4196

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 94454d6862..0aaaec32a8 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
@@ -1,7 +1,8 @@
 use std::mem::take;
 
+use indexmap::IndexMap;
 use smallvec::SmallVec;
-use swc_atoms::js_word;
+use swc_atoms::{js_word, JsWord};
 use swc_common::{
     chain, collections::AHashMap, util::take::Take, Mark, Spanned, SyntaxContext, DUMMY_SP,
 };
@@ -176,9 +177,9 @@ impl BlockScoping {
 
             let mut flow_helper = FlowHelper {
                 all: &args,
-                has_continue: false,
                 has_break: false,
                 has_return: false,
+                label: IndexMap::new(),
                 mutated,
                 in_switch_case: false,
                 in_nested_loop: false,
@@ -264,7 +265,7 @@ impl BlockScoping {
                 type_args: None,
             };
 
-            if flow_helper.has_return || flow_helper.has_continue || flow_helper.has_break {
+            if flow_helper.has_return || flow_helper.has_break || !flow_helper.label.is_empty() {
                 let ret = private_ident!("_ret");
 
                 let mut stmts = vec![
@@ -282,11 +283,9 @@ impl BlockScoping {
                     })),
                 ];
 
-                let use_switch = flow_helper.has_break && flow_helper.has_continue;
-
-                let check_ret = if flow_helper.has_return {
+                if flow_helper.has_return {
                     // if (_typeof(_ret) === "object") return _ret.v;
-                    Some(
+                    stmts.push(
                         IfStmt {
                             span: DUMMY_SP,
                             test: Box::new(Expr::Bin(BinExpr {
@@ -315,88 +314,51 @@ impl BlockScoping {
                         }
                         .into(),
                     )
-                } else {
-                    None
-                };
-
-                if use_switch {
-                    let mut cases = vec![];
-
-                    if flow_helper.has_break {
-                        cases.push(SwitchCase {
-                            span: DUMMY_SP,
-                            test: Some(Box::new(quote_str!("break").into())),
-                            // TODO: Handle labelled statements
-                            cons: vec![Stmt::Break(BreakStmt {
-                                span: DUMMY_SP,
-                                label: None,
-                            })],
-                        });
-                    }
+                }
 
-                    if flow_helper.has_continue {
-                        cases.push(SwitchCase {
+                if flow_helper.has_break {
+                    stmts.push(
+                        IfStmt {
                             span: DUMMY_SP,
-                            test: Some(Box::new(quote_str!("continue").into())),
-                            // TODO: Handle labelled statements
-                            cons: vec![Stmt::Continue(ContinueStmt {
+                            test: ret.clone().make_eq(quote_str!("break")).into(),
+                            cons: Stmt::Break(BreakStmt {
                                 span: DUMMY_SP,
                                 label: None,
-                            })],
-                        });
-                    }
-
-                    cases.extend(check_ret.map(|stmt| SwitchCase {
-                        span: DUMMY_SP,
-                        test: None,
-                        cons: vec![stmt],
-                    }));
+                            })
+                            .into(),
+                            alt: None,
+                        }
+                        .into(),
+                    );
+                }
 
+                if !flow_helper.label.is_empty() {
                     stmts.push(
                         SwitchStmt {
                             span: DUMMY_SP,
                             discriminant: Box::new(ret.into()),
-                            cases,
+                            cases: flow_helper
+                                .label
+                                .into_iter()
+                                .map(|(key, label)| SwitchCase {
+                                    span: DUMMY_SP,
+                                    test: Some(Box::new(key.into())),
+                                    cons: vec![match label {
+                                        Label::Break(id) => Stmt::Break(BreakStmt {
+                                            span: DUMMY_SP,
+                                            label: Some(id),
+                                        }),
+
+                                        Label::Continue(id) => Stmt::Continue(ContinueStmt {
+                                            span: DUMMY_SP,
+                                            label: Some(id),
+                                        }),
+                                    }],
+                                })
+                                .collect(),
                         }
                         .into(),
                     );
-                } else {
-                    //
-                    if flow_helper.has_break {
-                        stmts.push(
-                            IfStmt {
-                                span: DUMMY_SP,
-                                test: ret.clone().make_eq(quote_str!("break")).into(),
-                                // TODO: Handle labelled statements
-                                cons: Stmt::Break(BreakStmt {
-                                    span: DUMMY_SP,
-                                    label: None,
-                                })
-                                .into(),
-                                alt: None,
-                            }
-                            .into(),
-                        );
-                    }
-
-                    if flow_helper.has_continue {
-                        stmts.push(
-                            IfStmt {
-                                span: DUMMY_SP,
-                                test: ret.make_eq(quote_str!("continue")).into(),
-                                // TODO: Handle labelled statements
-                                cons: Stmt::Continue(ContinueStmt {
-                                    span: DUMMY_SP,
-                                    label: None,
-                                })
-                                .into(),
-                                alt: None,
-                            }
-                            .into(),
-                        );
-                    }
-
-                    stmts.extend(check_ret);
                 }
 
                 *body = Box::new(
@@ -715,11 +677,11 @@ impl Visit for InfectionFinder<'_> {
     }
 }
 
-#[derive(Debug)]
 struct FlowHelper<'a> {
-    has_continue: bool,
     has_break: bool,
     has_return: bool,
+    // label cannot be shadowed, so it's pretty safe to use JsWord
+    label: IndexMap<JsWord, Label>,
     all: &'a Vec<Id>,
     mutated: AHashMap<Id, SyntaxContext>,
     in_switch_case: bool,
@@ -727,6 +689,11 @@ struct FlowHelper<'a> {
     in_nested_loop: bool,
 }
 
+enum Label {
+    Break(Ident),
+    Continue(Ident),
+}
+
 impl<'a> FlowHelper<'a> {
     fn check(&mut self, i: Id) {
         if self.all.contains(&i) {
@@ -803,34 +770,50 @@ impl VisitMut for FlowHelper<'_> {
         let span = node.span();
 
         match node {
-            Stmt::Continue(..) => {
+            Stmt::Continue(ContinueStmt { label, .. }) => {
                 if self.in_nested_loop {
                     return;
                 }
 
-                self.has_continue = true;
+                let value = if let Some(label) = label {
+                    let value: JsWord = format!("continue|{}", label.sym).into();
+                    self.label
+                        .insert(value.clone(), Label::Continue(label.clone()));
+                    value
+                } else {
+                    "continue".into()
+                };
+
                 *node = Stmt::Return(ReturnStmt {
                     span,
                     arg: Some(
                         Expr::Lit(Lit::Str(Str {
                             span,
-                            value: "continue".into(),
+                            value,
                             raw: None,
                         }))
                         .into(),
                     ),
                 });
             }
-            Stmt::Break(..) => {
+            Stmt::Break(BreakStmt { label, .. }) => {
                 if self.in_switch_case || self.in_nested_loop {
                     return;
                 }
-                self.has_break = true;
+                let value = if let Some(label) = label {
+                    let value: JsWord = format!("break|{}", label.sym).into();
+                    self.label
+                        .insert(value.clone(), Label::Break(label.clone()));
+                    value
+                } else {
+                    self.has_break = true;
+                    "break".into()
+                };
                 *node = Stmt::Return(ReturnStmt {
                     span,
                     arg: Some(Box::new(Expr::Lit(Lit::Str(Str {
                         span,
-                        value: "break".into(),
+                        value,
                         raw: None,
                     })))),
                 });


Test patch:
diff --git a/crates/swc/tests/fixture/issues-3xxx/issue-3417/1/output/index.ts b/crates/swc/tests/fixture/issues-3xxx/issue-3417/1/output/index.ts
index 7001b38a89..85d23db7a1 100644
--- a/crates/swc/tests/fixture/issues-3xxx/issue-3417/1/output/index.ts
+++ b/crates/swc/tests/fixture/issues-3xxx/issue-3417/1/output/index.ts
@@ -79,10 +79,7 @@ export function selectRooms(building) {
                         FLOOR_MAPPING[levelId], 
                     ];
                 };
-                for(var _iterator2 = sortedRooms[Symbol.iterator](), _step2; !(_iteratorNormalCompletion1 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion1 = true){
-                    var _ret = _loop(_iterator2, _step2);
-                    if (_ret === "continue") continue;
-                }
+                for(var _iterator2 = sortedRooms[Symbol.iterator](), _step2; !(_iteratorNormalCompletion1 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion1 = true)_loop(_iterator2, _step2);
             } catch (err) {
                 _didIteratorError1 = true;
                 _iteratorError1 = err;
diff --git a/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs b/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
index 3604061e2d..ccd4fc818f 100644
--- a/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
+++ b/crates/swc_ecma_transforms_compat/tests/es2015_block_scoping.rs
@@ -241,10 +241,7 @@ test!(
             };
             var vars = [];
             var elem = null, name, val;
-            for(var i = 0; i < this.elements.length; i++){
-                var _ret = _loop(i);
-                if (_ret === \"continue\") continue;
-            }
+            for(var i = 0; i < this.elements.length; i++)_loop(i);
             return vars;
         };
         "
@@ -361,10 +358,7 @@ test!(
             if (i1 % 2 === 0) return i = i1, "continue";
             i = i1, void 0;
         };
-        for(var i = 0; i < 5; i++){
-            var _ret = _loop(i);
-            if (_ret === "continue") continue;
-        }
+        for(var i = 0; i < 5; i++)_loop(i);
         "#
 );
 
@@ -998,6 +992,75 @@ test!(
     "
 );
 
+test!(
+    ::swc_ecma_parser::Syntax::default(),
+    |_| block_scoping(),
+    issue_4196,
+    "
+    for (let i = 0; i < 2; i++) {
+        if (i === 0) continue
+        if (i === 1) break
+
+        [1].forEach((_) => {
+          console.log(i)
+        })
+    }
+    ",
+    r#"
+    var _loop = function(i) {
+        if (i === 0) return "continue";
+        if (i === 1) return "break";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i);
+        });
+    };
+    for(var i = 0; i < 2; i++){
+        var _ret = _loop(i);
+        if (_ret === "break") break;
+    }
+    "#
+);
+
+test!(
+    ::swc_ecma_parser::Syntax::default(),
+    |_| block_scoping(),
+    labeled_break,
+    "
+    a:
+    b:
+    for (let i = 0; i < 2; i++) {
+        if (i === 0) continue a
+        if (i === 1) break b
+
+        [1].forEach((_) => {
+          console.log(i)
+        })
+    }
+    ",
+    r#"
+    var _loop = function(i) {
+        if (i === 0) return "continue|a";
+        if (i === 1) return "break|b";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i);
+        });
+    };
+    a: b: for(var i = 0; i < 2; i++){
+        var _ret = _loop(i);
+        switch(_ret){
+            case "continue|a":
+                continue a;
+            case "break|b":
+                break b;
+        }
+    }
+    "#
+);
+
 test_exec!(
     ::swc_ecma_parser::Syntax::default(),
     |_| block_scoping(),