Repository: swc-project/swc (rust)

Issue:
Nested break/continue labels are broken when transforming to es5 
### Describe the bug

output code

```JavaScript
var _loop = function(i) {
    var _loop1 = function(j) {
        if (i < 2) return "continue|out";
        [
            1
        ].forEach(function(_) {
            console.log(i, j);
        });
    };
    for(var j = 0; j < 4; ++j){
        var _ret = _loop1(j);
        switch(_ret){
            case "continue|out":
                continue out;
        }
    }
};
out: for(var i = 0; i < 4; i++)_loop(i);
```

### Input code

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


### Config

```json
{
  "sourceMaps": true,
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "preserveAllComments": true,
    "transform": null,
    "target": "es5",
    "loose": false,
    "externalHelpers": false,
    "keepClassNames": false,
    "minify": {
      "compress": false
    }
  },
  "isModule": true
}
```


### Playground link

[REPL](https://play.swc.rs/?version=1.2.163&code=H4sIAAAAAAAAA8svLbFSSMsvUtDISS1RyFSwVTCwBlI2CiZASltbU6GaSwEI4CqyICqyICq0tbNgKkAgM01BA6TXSFMhOT%2BvJDOvNFUhv7TEmguuItowVg9olmticoaGRrymgq0dknYQAOorzs9J1cvJT9fI1FHI0rSGS9dC2bVctQAQtOELtgAAAA%3D%3D&config=H4sIAAAAAAAAA0WOOw7CQAwF7%2BI6Lc12KA1NuIO1vKCA9yN7gxJFuTu7AkJnjZ9Gs5GlWT0Gzkau6IyOHubJbZRZDdouW2PhhRzBBzavUy7UZhWNLIa9o6yo4xfOIn0KAbEcuqIcbUwayMVZpALWO0rT2al6JCXD19QRlgKNLBdIhtrBn0Duhc2uHPDHYYrTuLZIn0KL%2BL32GjXZkG6z4FOyvwEeTGy97AAAAA%3D%3D)

### Expected behavior

print
```
2 0
2 1
2 2
2 3
3 0
3 1
3 2
3 3
```

### Actual behavior

Uncaught SyntaxError: Undefined label 'out'

### Version

1.2.163

### Additional context

_No response_

Pull request:
fix(es/compat): nested for loop with break/continue
<!-- 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):**
closes #4238, this time really

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 6cce0e90f5..665c9f0cfa 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,5 +1,6 @@
 use std::mem::take;
 
+use ahash::AHashSet;
 use indexmap::IndexMap;
 use smallvec::SmallVec;
 use swc_atoms::{js_word, JsWord};
@@ -181,6 +182,7 @@ impl BlockScoping {
                 has_break: false,
                 has_return: false,
                 label: IndexMap::new(),
+                inner_label: AHashSet::new(),
                 mutated,
                 in_switch_case: false,
                 in_nested_loop: false,
@@ -707,6 +709,7 @@ struct FlowHelper<'a> {
     has_return: bool,
     // label cannot be shadowed, so it's pretty safe to use JsWord
     label: IndexMap<JsWord, Label>,
+    inner_label: AHashSet<JsWord>,
     all: &'a Vec<Id>,
     mutated: AHashMap<Id, SyntaxContext>,
     in_switch_case: bool,
@@ -728,6 +731,13 @@ impl<'a> FlowHelper<'a> {
             );
         }
     }
+
+    fn has_outer_label(&self, label: &Option<Ident>) -> bool {
+        match label {
+            Some(l) => self.inner_label.get(&l.sym).is_none(),
+            None => false,
+        }
+    }
 }
 
 #[swc_trace]
@@ -737,6 +747,12 @@ impl VisitMut for FlowHelper<'_> {
     /// noop
     fn visit_mut_arrow_expr(&mut self, _n: &mut ArrowExpr) {}
 
+    fn visit_mut_labeled_stmt(&mut self, l: &mut LabeledStmt) {
+        self.inner_label.insert(l.label.sym.clone());
+
+        l.visit_mut_children_with(self);
+    }
+
     fn visit_mut_assign_expr(&mut self, n: &mut AssignExpr) {
         match &n.left {
             PatOrExpr::Expr(e) => {
@@ -796,6 +812,9 @@ impl VisitMut for FlowHelper<'_> {
 
         match node {
             Stmt::Continue(ContinueStmt { label, .. }) => {
+                if self.in_nested_loop && !self.has_outer_label(label) {
+                    return;
+                }
                 let value = if let Some(label) = label {
                     let value: JsWord = format!("continue|{}", label.sym).into();
                     self.label
@@ -818,7 +837,7 @@ impl VisitMut for FlowHelper<'_> {
                 });
             }
             Stmt::Break(BreakStmt { label, .. }) => {
-                if self.in_switch_case || self.in_nested_loop {
+                if (self.in_switch_case || self.in_nested_loop) && !self.has_outer_label(label) {
                     return;
                 }
                 let value = if let Some(label) = label {


Test patch:
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/input.js
new file mode 100644
index 0000000000..4d2fd728fc
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/input.js
@@ -0,0 +1,9 @@
+for (let i = 0; i < 4; i++) {
+    for (let j = 0; j < 4; ++j) {
+        if (i < 2) continue;
+
+        [1].forEach((_) => {
+            console.log(i, j);
+        });
+    }
+}
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/output.js
new file mode 100644
index 0000000000..426f31b2d1
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/2/output.js
@@ -0,0 +1,12 @@
+var _loop = function(i) {
+    var _loop1 = function(j) {
+        if (i < 2) return "continue";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i, j);
+        });
+    };
+    for(var j = 0; j < 4; ++j)_loop1(j);
+};
+for(var i = 0; i < 4; i++)_loop(i);
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/input.js
new file mode 100644
index 0000000000..f04ddaa61b
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/input.js
@@ -0,0 +1,9 @@
+out: for (let i = 0; i < 4; i++) {
+    for (let j = 0; j < 4; ++j) {
+        if (i > 2) break out;
+
+        [1].forEach((_) => {
+            console.log(i, j);
+        });
+    }
+}
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/output.js
new file mode 100644
index 0000000000..16fb4a2952
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/3/output.js
@@ -0,0 +1,24 @@
+var _loop = function(i) {
+    var _loop1 = function(j) {
+        if (i > 2) return "break|out";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i, j);
+        });
+    };
+    for(var j = 0; j < 4; ++j){
+        var _ret1 = _loop1(j);
+        switch(_ret1){
+            case "break|out":
+                return "break|out";
+        }
+    }
+};
+out: for(var i = 0; i < 4; i++){
+    var _ret = _loop(i);
+    switch(_ret){
+        case "break|out":
+            break out;
+    }
+}
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/input.js
new file mode 100644
index 0000000000..acb4429bcb
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/input.js
@@ -0,0 +1,9 @@
+for (let i = 0; i < 4; i++) {
+    for (let j = 0; j < 4; ++j) {
+        if (i > 2) break;
+
+        [1].forEach((_) => {
+            console.log(i, j);
+        });
+    }
+}
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/output.js
new file mode 100644
index 0000000000..eef27d4364
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/4/output.js
@@ -0,0 +1,15 @@
+var _loop = function(i) {
+    var _loop1 = function(j) {
+        if (i > 2) return "break";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i, j);
+        });
+    };
+    for(var j = 0; j < 4; ++j){
+        var _ret = _loop1(j);
+        if (_ret === "break") break;
+    }
+};
+for(var i = 0; i < 4; i++)_loop(i);
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/input.js
new file mode 100644
index 0000000000..106cd68496
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/input.js
@@ -0,0 +1,9 @@
+for (let i = 0; i < 4; i++) {
+    b: for (let j = 0; j < 4; ++j) {
+        if (i < 2) continue b;
+
+        [1].forEach((_) => {
+            console.log(i, j);
+        });
+    }
+}
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/output.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/output.js
new file mode 100644
index 0000000000..9505e9caa4
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-4238/5/output.js
@@ -0,0 +1,18 @@
+var _loop = function(i) {
+    var _loop1 = function(j) {
+        if (i < 2) return "continue|b";
+        [
+            1
+        ].forEach((_)=>{
+            console.log(i, j);
+        });
+    };
+    b: for(var j = 0; j < 4; ++j){
+        var _ret = _loop1(j);
+        switch(_ret){
+            case "continue|b":
+                continue b;
+        }
+    }
+};
+for(var i = 0; i < 4; i++)_loop(i);