Repository: swc-project/swc (rust)

Issue:
variable is declared multiple times when variable is reserved and there are 2 passes and target is >=es2015
### Describe the bug

When `jsc.minify.compress.passes > 1` and `jsx.minify.mangle.reserved` includes a reserved name - SWC can produce code that semantically invalid with the same variable being definied multiple times.

### Input code

```typescript
export function foo(cond) {
  let reserved = 1;
  if (cond) {
    reserved = 2;
  }
  return [reserved, bar(cond)];
}

function bar(cond) {
  let reserved = 1;
  if (cond) {
    reserved = 2;
  }
  return reserved;
}
```


### Config

```json
{
  "jsc": {
    "parser": {
      "syntax": "ecmascript",
      "jsx": false
    },
    "target": "es2015",
    "loose": false,
    "minify": {
      "compress": {
        "passes": 2
      },
      "mangle": {
        "reserved": ["reserved"]
      }
    }
  },
  "module": {
    "type": "es6"
  },
  "minify": false,
  "isModule": true
}
```


### Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.4.0&code=H4sIAAAAAAAAA0utKMgvKlFIK81LLsnMz1NIy8%2FXSM7PS9FUqOZSUMhJLVEoSi1OLSpLTVGwVTC0BoplpikgqVBAljcCyddygcRKSovyFKJhcjoKSYlFEF2x1ly1XFxw%2B%2BDi2OwzJmCfCZp9MCmQFQByNT4O2QAAAA%3D%3D&config=H4sIAAAAAAAAA1WOQQ6DIBBF956CzLqL1qRd9A49QdMFwdFgQMgMNhrj3YsoajeEeX%2F%2Bnz8VQkDLCp5iit84eEmMtM%2BR8NgFOUQCqKxkRdoHuGS15UWqpWFMaF4VCJIaDMnF5fV23xxgnGPMjo1Z3el6PN9UznpC5hNL3ZhxYeXG5r2GlV1j8H89BiB9sYr0fQyf7C3ym1LAuqo%2FJUAYPa71H3As5aZ7fdD8ys5APRbzD1wt6BJUAQAA

### SWC Info output

_No response_

### Expected behavior

SWC produces valid code

### Actual behavior

SWC produces broken code:
```js
export function foo(e) {
    let reserved, reserved = 1;
    return e && (reserved = 2), [
        reserved,
        (reserved = 1, e && (reserved = 2), reserved)
    ];
}
```

### Version

1.4.0

### Additional context

Note: When targetting es5 the output uses `var` so it does not crash in a browser.


For comparison the 1 pass code:
```js
export function foo(e) {
    let reserved = 1;
    return e && (reserved = 2), [
        reserved,
        function(e) {
            let reserved = 1;
            return e && (reserved = 2), reserved;
        }(e)
    ];
}
```

1 pass code with no reserved names:
```js
export function foo(t) {
    let n = 1;
    return t && (n = 2), [
        n,
        function(t) {
            let n = 1;
            return t && (n = 2), n;
        }(t)
    ];
}
```

2 pass code with no reserved names:
```js
export function foo(o) {
    let t, e = 1;
    return o && (e = 2), [
        e,
        (t = 1, o && (t = 2), t)
    ];
}
```


Pull request description:
fix(es/minifier): Don't invoke IIFE containing reserved words
**Description:**

`inline` and `seq inline` should apply this change too.


**Related issue:**

 - Closes #8622

Patch:
diff --git a/crates/swc_ecma_minifier/src/compress/optimize/iife.rs b/crates/swc_ecma_minifier/src/compress/optimize/iife.rs
index fdc82e1123..44d160493f 100644
--- a/crates/swc_ecma_minifier/src/compress/optimize/iife.rs
+++ b/crates/swc_ecma_minifier/src/compress/optimize/iife.rs
@@ -702,6 +702,16 @@ impl Optimizer<'_> {
             }
         }
 
+        for pid in param_ids {
+            if self.ident_reserved(&pid.sym) {
+                log_abort!(
+                    "iife: [x] Cannot inline because of reservation of `{}`",
+                    pid
+                );
+                return false;
+            }
+        }
+
         true
     }
 
@@ -751,83 +761,95 @@ impl Optimizer<'_> {
             }
         }
 
-        if !body.stmts.iter().all(|stmt| match stmt {
-            Stmt::Decl(Decl::Var(var))
-                if matches!(
-                    &**var,
-                    VarDecl {
-                        kind: VarDeclKind::Var | VarDeclKind::Let,
-                        ..
+        if !body.stmts.iter().all(|stmt| {
+            if let Stmt::Decl(Decl::Var(var)) = stmt {
+                for decl in &var.decls {
+                    for id in find_pat_ids::<_, Id>(&decl.name) {
+                        if self.ident_reserved(&id.0) {
+                            log_abort!("iife: [x] Cannot inline because reservation of `{}`", id.0);
+                            return false;
+                        }
                     }
-                ) =>
-            {
-                if var.decls.iter().any(|decl| match &decl.name {
-                    Pat::Ident(BindingIdent {
-                        id: Ident { sym, .. },
-                        ..
-                    }) if &**sym == "arguments" => true,
-                    Pat::Ident(id) => {
-                        if self.vars.has_pending_inline_for(&id.to_id()) {
-                            log_abort!(
-                                "iife: [x] Cannot inline because pending inline of `{}`",
-                                id.id
-                            );
-                            return true;
+                }
+            }
+
+            match stmt {
+                Stmt::Decl(Decl::Var(var))
+                    if matches!(
+                        &**var,
+                        VarDecl {
+                            kind: VarDeclKind::Var | VarDeclKind::Let,
+                            ..
                         }
+                    ) =>
+                {
+                    for decl in &var.decls {
+                        match &decl.name {
+                            Pat::Ident(BindingIdent {
+                                id: Ident { sym, .. },
+                                ..
+                            }) if &**sym == "arguments" => return false,
+                            Pat::Ident(id) => {
+                                if self.vars.has_pending_inline_for(&id.to_id()) {
+                                    log_abort!(
+                                        "iife: [x] Cannot inline because pending inline of `{}`",
+                                        id.id
+                                    );
+                                    return false;
+                                }
+                            }
 
-                        false
+                            _ => return false,
+                        }
                     }
 
-                    _ => true,
-                }) {
-                    return false;
-                }
+                    if self.ctx.executed_multiple_time {
+                        return false;
+                    }
 
-                if self.ctx.executed_multiple_time {
-                    return false;
-                }
+                    if !self.may_add_ident() {
+                        return false;
+                    }
 
-                if !self.may_add_ident() {
-                    return false;
+                    true
                 }
 
-                true
-            }
+                Stmt::Expr(e) => match &*e.expr {
+                    Expr::Await(..) => false,
 
-            Stmt::Expr(e) => match &*e.expr {
-                Expr::Await(..) => false,
+                    // TODO: Check if parameter is used and inline if call is not related to
+                    // parameters.
+                    Expr::Call(e) => {
+                        if e.callee.as_expr().and_then(|e| e.as_ident()).is_some() {
+                            return true;
+                        }
 
-                // TODO: Check if parameter is used and inline if call is not related to parameters.
-                Expr::Call(e) => {
-                    if e.callee.as_expr().and_then(|e| e.as_ident()).is_some() {
-                        return true;
-                    }
+                        let used = idents_used_by(&e.callee);
 
-                    let used = idents_used_by(&e.callee);
+                        if used.iter().all(|id| {
+                            self.data
+                                .vars
+                                .get(id)
+                                .map(|usage| usage.ref_count == 1 && usage.callee_count > 0)
+                                .unwrap_or(false)
+                        }) {
+                            return true;
+                        }
 
-                    if used.iter().all(|id| {
-                        self.data
-                            .vars
-                            .get(id)
-                            .map(|usage| usage.ref_count == 1 && usage.callee_count > 0)
-                            .unwrap_or(false)
-                    }) {
-                        return true;
+                        param_ids.iter().all(|param| !used.contains(&param.to_id()))
                     }
 
-                    param_ids.iter().all(|param| !used.contains(&param.to_id()))
-                }
-
-                _ => true,
-            },
+                    _ => true,
+                },
 
-            Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
-                Some(Expr::Await(..)) => false,
+                Stmt::Return(ReturnStmt { arg, .. }) => match arg.as_deref() {
+                    Some(Expr::Await(..)) => false,
 
-                Some(Expr::Lit(Lit::Num(..))) => !self.ctx.in_obj_of_non_computed_member,
-                _ => true,
-            },
-            _ => false,
+                    Some(Expr::Lit(Lit::Num(..))) => !self.ctx.in_obj_of_non_computed_member,
+                    _ => true,
+                },
+                _ => false,
+            }
         }) {
             return false;
         }
diff --git a/crates/swc_ecma_minifier/src/compress/optimize/mod.rs b/crates/swc_ecma_minifier/src/compress/optimize/mod.rs
index 265cf9fdb4..09556b6c35 100644
--- a/crates/swc_ecma_minifier/src/compress/optimize/mod.rs
+++ b/crates/swc_ecma_minifier/src/compress/optimize/mod.rs
@@ -357,6 +357,14 @@ impl Optimizer<'_> {
         self.options.top_level()
     }
 
+    fn ident_reserved(&self, sym: &JsWord) -> bool {
+        if let Some(MangleOptions { reserved, .. }) = self.mangle_options {
+            reserved.contains(sym)
+        } else {
+            false
+        }
+    }
+
     fn handle_stmts(&mut self, stmts: &mut Vec<Stmt>, will_terminate: bool) {
         // Skip if `use asm` exists.
         if maybe_par!(


Test patch:
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/8622/input.js b/crates/swc_ecma_minifier/tests/fixture/issues/8622/input.js
new file mode 100644
index 0000000000..8113ce1e5b
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/8622/input.js
@@ -0,0 +1,15 @@
+export function foo(cond) {
+    let reserved = 1;
+    if (cond) {
+        reserved = 2;
+    }
+    return [reserved, bar(cond)];
+}
+
+function bar(cond) {
+    let reserved = 1;
+    if (cond) {
+        reserved = 2;
+    }
+    return reserved;
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/8622/mangle.json b/crates/swc_ecma_minifier/tests/fixture/issues/8622/mangle.json
new file mode 100644
index 0000000000..7a0990515d
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/8622/mangle.json
@@ -0,0 +1,3 @@
+{
+    "reserved": ["reserved"]
+}
diff --git a/crates/swc_ecma_minifier/tests/fixture/issues/8622/output.js b/crates/swc_ecma_minifier/tests/fixture/issues/8622/output.js
new file mode 100644
index 0000000000..6fedcd65f4
--- /dev/null
+++ b/crates/swc_ecma_minifier/tests/fixture/issues/8622/output.js
@@ -0,0 +1,10 @@
+export function foo(e) {
+    let reserved = 1;
+    return e && (reserved = 2), [
+        reserved,
+        function(e) {
+            let reserved = 1;
+            return e && (reserved = 2), reserved;
+        }(e)
+    ];
+}