Repository: swc-project/swc (rust)

Issue:
ES2015::block_scoping renames exports
### Describe the bug

I was writing [a how-to on using a new Sinon feature for auto-cleanup](https://github.com/sinonjs/sinon/pull/2556) of stubs injected using accessors when I discovered it did not work when I exposed accessors on an exported object called 'mock'. 

Basically, if I write something like this:
```typescript
export const mock = {
    get toBeMocked(){ return toBeMocked; },
    set toBeMocked(mock){ toBeMocked = mock; }
}
``` 

it will be transpiled to this by SWC, which is very different from `tsc`:
```typescript
export const _$mock = {
    get toBeMocked(){ return toBeMocked; },
    set toBeMocked(mock){ toBeMocked = mock; }
}
```

Somehow 'mock' gets turned into `_$mock`. 

To run this example, just do 
```
gh repo clone fatso83/sinon-swc-bug
cd sinon-swc-bug
git checkout swc-special-handling-mock
npm install
npm test
```

This is not a typescript issue, btw, as the [playground shows the issue with a minimal example](https://play.swc.rs/?version=1.3.94-nightly-20231016.1&code=H4sIAAAAAAAAA1WNQQrCMBBF93OKT1cteIPixp0LzyAlTmuwnZF0CkLJ3ZuIWrP9vP9ev4gzr4Kr6Ykv6h58qxusBAS2JQiqM7oJdmdo8IOXbkT%2FOVUtRSJ%2BPTUYRjbsDhz%2FjT%2FIqcyGKY0JyBFgKH51s37D%2B9giHt7sXLLZk%2FiimrfEU9wALRrz29oAAAA%3D&config=H4sIAAAAAAAAA1WPSw7DIAwF9zkF8rrbdtE79BAWdSIifrKJVBTl7iUE0maH3xsz8jooBbNoeKq1PMsQkYX4nEsi2Sf8lARIOxTNJia49XaWvRrRCtVoOxpIyBOluiX3hoMNQajjLXPGmzH%2FC3VwkUnkCu4o%2BsnSVTc0JbjwXmrZDkk50qF%2FwA%2FqsvNjMPLqm4kXGrYvhlQioBQBAAA%3D)

### Input code

```typescript
// The entire runnable example is here: 
// https://github.com/fatso83/sinon-swc-bug/tree/swc-special-handling-mock

function _toBeMocked() {
  return "I am the original function";
}

export let toBeMocked = _toBeMocked

export const mock = {
    get toBeMocked(){ return toBeMocked; },
    set toBeMocked(mock){ toBeMocked = mock; }
}
```


### Config

```json
{
    "test": ".*.ts$",
    "jsc": {
      "parser": {
        "syntax": "typescript",
        "decorators": true
      },
      "transform": {
        "legacyDecorator": true,
        "decoratorMetadata": true
      }
    },
    "module": {
      "type": "commonjs"
    }
  }
```


### Playground link (or link to the minimal reproduction)

https://play.swc.rs/?version=1.3.94-nightly-20231016.1&code=H4sIAAAAAAAAA1WNQQrCMBBF93OKT1cteIPixp0LzyAlTmuwnZF0CkLJ3ZuIWrP9vP9ev4gzr4Kr6Ykv6h58qxusBAS2JQiqM7oJdmdo8IOXbkT%2FOVUtRSJ%2BPTUYRjbsDhz%2FjT%2FIqcyGKY0JyBFgKH51s37D%2B9giHt7sXLLZk%2FiimrfEU9wALRrz29oAAAA%3D&config=H4sIAAAAAAAAA1WPSw7DIAwF9zkF8rrbdtE79BAWdSIifrKJVBTl7iUE0maH3xsz8jooBbNoeKq1PMsQkYX4nEsi2Sf8lARIOxTNJia49XaWvRrRCtVoOxpIyBOluiX3hoMNQajjLXPGmzH%2FC3VwkUnkCu4o%2BsnSVTc0JbjwXmrZDkk50qF%2FwA%2FqsvNjMPLqm4kXGrYvhlQioBQBAAA%3D

### SWC Info output


    Operating System:
        Platform: darwin
        Arch: arm64
        Machine Type: arm64
        Version: Darwin Kernel Version 23.0.0: Fri Sep 15 14:43:05 PDT 2023; root:xnu-10002.1.13~1/RELEASE_ARM64_T6020
        CPU: (12 cores)
            Models: Apple M2 Max

    Binaries:
        Node: 20.5.0
        npm: 9.8.0
        Yarn: 1.22.19
        pnpm: 7.13.0

    Relevant Packages:
        @swc/core: 1.3.76
        @swc/helpers: N/A
        @swc/types: N/A
        typescript: 5.1.6

    SWC Config:
        output: N/A
        .swcrc path: N/A

    Next.js info:
        output: N/A



### Expected behavior

This code:
```
export const mock = {
    get toBeMocked(){ return toBeMocked; },
    set toBeMocked(mock){ toBeMocked = mock; }
}
```
should be pretty much unaltered when compiled to ES5 with ESM exports. Probably something like this

```
function _toBeMocked() {
    return "I am the original function";
}
export var toBeMocked = _toBeMocked;
export var mock = {
    get toBeMocked () {
        return toBeMocked;
    },
    set toBeMocked (mock){
        toBeMocked = mock;
    }
};
```

### Actual behavior

The export is called `_$mock` instead of `mock`
```
function _toBeMocked() {
    return "I am the original function";
}
export var toBeMocked = _toBeMocked;
export var _$mock = {
    get toBeMocked () {
        return toBeMocked;
    },
    set toBeMocked (mock){
        toBeMocked = mock;
    }
};
```

### Version

1.3.94

### Additional context

[Originally posted as question](https://github.com/swc-project/swc/discussions/8143) on the Q&A.

Just [play with the playground](https://play.swc.rs/?version=1.3.94-nightly-20231016.1&code=H4sIAAAAAAAAA1WNQQrCMBBF93OKT1cteIPixp0LzyAlTmuwnZF0CkLJ3ZuIWrP9vP9ev4gzr4Kr6Ykv6h58qxusBAS2JQiqM7oJdmdo8IOXbkT%2FOVUtRSJ%2BPTUYRjbsDhz%2FjT%2FIqcyGKY0JyBFgKH51s37D%2B9giHt7sXLLZk%2FiimrfEU9wALRrz29oAAAA%3D&config=H4sIAAAAAAAAA1WPSw7DIAwF9zkF8rrbdtE79BAWdSIifrKJVBTl7iUE0maH3xsz8jooBbNoeKq1PMsQkYX4nEsi2Sf8lARIOxTNJia49XaWvRrRCtVoOxpIyBOluiX3hoMNQajjLXPGmzH%2FC3VwkUnkCu4o%2BsnSVTc0JbjwXmrZDkk50qF%2FwA%2FqsvNjMPLqm4kXGrYvhlQioBQBAAA%3D). It will show in any combination using ES5 as the target

Pull request description:
fix(es/compat): Make `block-scoping` pass rename exports correctly
**Description:**


**Related issue:**

 - Closes #8148

Patch:
diff --git a/crates/swc_ecma_compat_es2015/src/block_scoping/mod.rs b/crates/swc_ecma_compat_es2015/src/block_scoping/mod.rs
index 008b1e65fa..1c916586b5 100644
--- a/crates/swc_ecma_compat_es2015/src/block_scoping/mod.rs
+++ b/crates/swc_ecma_compat_es2015/src/block_scoping/mod.rs
@@ -21,7 +21,6 @@ use swc_ecma_visit::{
 };
 use swc_trace_macro::swc_trace;
 
-mod operator;
 mod vars;
 
 ///
diff --git a/crates/swc_ecma_compat_es2015/src/block_scoping/operator.rs b/crates/swc_ecma_compat_es2015/src/block_scoping/operator.rs
deleted file mode 100644
index 5e5b1fbe65..0000000000
--- a/crates/swc_ecma_compat_es2015/src/block_scoping/operator.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-use swc_common::{collections::AHashMap, SyntaxContext, DUMMY_SP};
-use swc_ecma_ast::*;
-use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
-use swc_trace_macro::swc_trace;
-
-pub(super) fn rename(map: AHashMap<Id, Id>) -> Rename {
-    Rename { map }
-}
-
-pub(super) struct Rename {
-    map: AHashMap<Id, Id>,
-}
-
-#[swc_trace]
-impl VisitMut for Rename {
-    noop_visit_mut_type!();
-
-    fn visit_mut_ident(&mut self, i: &mut Ident) {
-        // fast path.
-        if i.span.ctxt == SyntaxContext::empty() {
-            return;
-        }
-
-        if let Some(id) = self.map.get(&i.to_id()) {
-            *i = Ident::new(id.0.clone(), i.span.with_ctxt(id.1));
-        }
-    }
-
-    fn visit_mut_member_prop(&mut self, n: &mut MemberProp) {
-        if let MemberProp::Computed(n) = n {
-            n.visit_mut_with(self);
-        }
-    }
-
-    fn visit_mut_object_pat_prop(&mut self, i: &mut ObjectPatProp) {
-        match i {
-            ObjectPatProp::Assign(p) => {
-                p.value.visit_mut_with(self);
-
-                let orig = p.key.clone();
-                p.key.visit_mut_with(self);
-
-                if orig.to_id() == p.key.to_id() {
-                    return;
-                }
-
-                match p.value.take() {
-                    Some(default) => {
-                        *i = ObjectPatProp::KeyValue(KeyValuePatProp {
-                            key: PropName::Ident(orig),
-                            value: Box::new(Pat::Assign(AssignPat {
-                                span: DUMMY_SP,
-                                left: Box::new(Pat::Ident(p.key.clone().into())),
-                                right: default,
-                            })),
-                        });
-                    }
-                    None => {
-                        *i = ObjectPatProp::KeyValue(KeyValuePatProp {
-                            key: PropName::Ident(orig),
-                            value: Box::new(Pat::Ident(p.key.clone().into())),
-                        });
-                    }
-                }
-            }
-
-            _ => {
-                i.visit_mut_children_with(self);
-            }
-        }
-    }
-
-    fn visit_mut_prop(&mut self, n: &mut Prop) {
-        if let Prop::Shorthand(ident) = n {
-            if let Some(id) = self.map.get(&ident.to_id()) {
-                *n = KeyValueProp {
-                    key: PropName::Ident(ident.clone()),
-                    value: Box::new(Ident::new(id.0.clone(), ident.span.with_ctxt(id.1)).into()),
-                }
-                .into();
-            }
-
-            return;
-        }
-
-        n.visit_mut_children_with(self);
-    }
-
-    fn visit_mut_prop_name(&mut self, n: &mut PropName) {
-        if let PropName::Computed(n) = n {
-            n.visit_mut_with(self);
-        }
-    }
-}
diff --git a/crates/swc_ecma_compat_es2015/src/block_scoping/vars.rs b/crates/swc_ecma_compat_es2015/src/block_scoping/vars.rs
index c9d26639a1..f00c79b9af 100644
--- a/crates/swc_ecma_compat_es2015/src/block_scoping/vars.rs
+++ b/crates/swc_ecma_compat_es2015/src/block_scoping/vars.rs
@@ -5,12 +5,10 @@ use swc_common::{
     Mark, SyntaxContext,
 };
 use swc_ecma_ast::*;
-use swc_ecma_transforms_base::scope::ScopeKind;
+use swc_ecma_transforms_base::{rename::remap, scope::ScopeKind};
 use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
 use swc_trace_macro::swc_trace;
 
-use super::operator::{rename, Rename};
-
 pub(super) fn block_scoped_vars() -> impl VisitMut {
     BlockScopedVars::default()
 }
@@ -62,7 +60,7 @@ impl BlockScopedVars {
     ///  - For third, we rename all declarations which may conflict.
     fn handle_program<N>(&mut self, n: &mut N)
     where
-        N: VisitMutWith<Self> + VisitMutWith<Rename>,
+        N: VisitMutWith<Self> + for<'aa> VisitMutWith<dyn 'aa + VisitMut>,
     {
         n.visit_mut_children_with(self);
 
@@ -81,7 +79,7 @@ impl BlockScopedVars {
 
         // dbg!(&rename_map);
 
-        n.visit_mut_with(&mut rename(rename_map))
+        n.visit_mut_with(&mut remap(&rename_map, Default::default()) as &mut dyn VisitMut);
     }
 
     fn with_scope(&mut self, kind: ScopeKind, op: impl FnOnce(&mut Self)) {
diff --git a/crates/swc_ecma_transforms_base/src/rename/mod.rs b/crates/swc_ecma_transforms_base/src/rename/mod.rs
index 09d1e50030..2f00233b56 100644
--- a/crates/swc_ecma_transforms_base/src/rename/mod.rs
+++ b/crates/swc_ecma_transforms_base/src/rename/mod.rs
@@ -54,6 +54,14 @@ pub fn rename_with_config(map: &AHashMap<Id, JsWord>, config: Config) -> impl '_
     })
 }
 
+pub fn remap(map: &AHashMap<Id, Id>, config: Config) -> impl '_ + Fold + VisitMut {
+    as_folder(Operator {
+        rename: map,
+        config,
+        extra: Default::default(),
+    })
+}
+
 pub fn renamer<R>(config: Config, renamer: R) -> impl Fold + VisitMut
 where
     R: Renamer,
diff --git a/crates/swc_ecma_transforms_base/src/rename/ops.rs b/crates/swc_ecma_transforms_base/src/rename/ops.rs
index c2e35bf962..078f1bae8b 100644
--- a/crates/swc_ecma_transforms_base/src/rename/ops.rs
+++ b/crates/swc_ecma_transforms_base/src/rename/ops.rs
@@ -1,10 +1,10 @@
-use swc_atoms::JsWord;
 use swc_common::{
     collections::AHashMap,
     util::{move_map::MoveMap, take::Take},
     Spanned, SyntaxContext, DUMMY_SP,
 };
 use swc_ecma_ast::*;
+use swc_ecma_utils::ident::IdentLike;
 use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
 
 use crate::{
@@ -12,14 +12,20 @@ use crate::{
     perf::{cpu_count, ParExplode, Parallel, ParallelExt},
 };
 
-pub(super) struct Operator<'a> {
-    pub rename: &'a AHashMap<Id, JsWord>,
+pub(super) struct Operator<'a, I>
+where
+    I: IdentLike,
+{
+    pub rename: &'a AHashMap<Id, I>,
     pub config: Config,
 
     pub extra: Vec<ModuleItem>,
 }
 
-impl Operator<'_> {
+impl<I> Operator<'_, I>
+where
+    I: IdentLike,
+{
     fn keep_class_name(&mut self, ident: &mut Ident, class: &mut Class) -> Option<ClassExpr> {
         if !self.config.keep_class_names {
             return None;
@@ -55,7 +61,10 @@ impl Operator<'_> {
     }
 }
 
-impl Parallel for Operator<'_> {
+impl<I> Parallel for Operator<'_, I>
+where
+    I: IdentLike,
+{
     fn create(&self) -> Self {
         Self {
             rename: self.rename,
@@ -73,7 +82,10 @@ impl Parallel for Operator<'_> {
     }
 }
 
-impl ParExplode for Operator<'_> {
+impl<I> ParExplode for Operator<'_, I>
+where
+    I: IdentLike,
+{
     fn after_one_stmt(&mut self, _: &mut Vec<Stmt>) {}
 
     fn after_one_module_item(&mut self, stmts: &mut Vec<ModuleItem>) {
@@ -81,7 +93,10 @@ impl ParExplode for Operator<'_> {
     }
 }
 
-impl<'a> VisitMut for Operator<'a> {
+impl<'a, I> VisitMut for Operator<'a, I>
+where
+    I: IdentLike,
+{
     noop_visit_mut_type!();
 
     /// Preserve key of properties.
@@ -576,12 +591,18 @@ impl<'a> VisitMut for Operator<'a> {
     }
 }
 
-struct VarFolder<'a, 'b> {
-    orig: &'a mut Operator<'b>,
+struct VarFolder<'a, 'b, I>
+where
+    I: IdentLike,
+{
+    orig: &'a mut Operator<'b, I>,
     renamed: &'a mut Vec<ExportSpecifier>,
 }
 
-impl VisitMut for VarFolder<'_, '_> {
+impl<I> VisitMut for VarFolder<'_, '_, I>
+where
+    I: IdentLike,
+{
     noop_visit_mut_type!();
 
     #[inline]
@@ -601,16 +622,21 @@ impl VisitMut for VarFolder<'_, '_> {
     }
 }
 
-impl<'a> Operator<'a> {
+impl<'a, I> Operator<'a, I>
+where
+    I: IdentLike,
+{
     /// Returns `Ok(renamed_ident)` if ident should be renamed.
     fn rename_ident(&mut self, ident: &mut Ident) -> Result<(), ()> {
-        if let Some(sym) = self.rename.get(&ident.to_id()) {
-            if *sym == ident.sym {
+        if let Some(new_id) = self.rename.get(&ident.to_id()) {
+            let (new_sym, new_ctxt) = new_id.to_id();
+
+            if new_sym == ident.sym {
                 return Err(());
             }
 
-            ident.span = ident.span.with_ctxt(SyntaxContext::empty());
-            ident.sym = sym.clone();
+            ident.span = ident.span.with_ctxt(new_ctxt);
+            ident.sym = new_sym;
             return Ok(());
         }
 
diff --git a/crates/swc_ecma_utils/src/ident.rs b/crates/swc_ecma_utils/src/ident.rs
index 43758daa8f..ff621e4faf 100644
--- a/crates/swc_ecma_utils/src/ident.rs
+++ b/crates/swc_ecma_utils/src/ident.rs
@@ -2,12 +2,26 @@ use swc_atoms::JsWord;
 use swc_common::{Span, SyntaxContext};
 use swc_ecma_ast::{BindingIdent, Id, Ident};
 
-pub trait IdentLike: Sized {
+pub trait IdentLike: Sized + Send + Sync + 'static {
     fn from_ident(i: &Ident) -> Self;
     fn to_id(&self) -> Id;
     fn into_id(self) -> Id;
 }
 
+impl IdentLike for JsWord {
+    fn from_ident(i: &Ident) -> Self {
+        i.sym.clone()
+    }
+
+    fn to_id(&self) -> Id {
+        (self.clone(), Default::default())
+    }
+
+    fn into_id(self) -> Id {
+        (self, Default::default())
+    }
+}
+
 impl IdentLike for BindingIdent {
     fn from_ident(i: &Ident) -> Self {
         i.clone().into()


Test patch:
diff --git a/crates/swc/tests/fixture/issues-8xxx/8148/input/.swcrc b/crates/swc/tests/fixture/issues-8xxx/8148/input/.swcrc
new file mode 100644
index 0000000000..daa38ba90c
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8148/input/.swcrc
@@ -0,0 +1,19 @@
+{
+    "jsc": {
+        "parser": {
+            "syntax": "ecmascript",
+            "jsx": false
+        },
+        "target": "es5",
+        "loose": false,
+        "minify": {
+            "compress": false,
+            "mangle": false
+        }
+    },
+    "module": {
+        "type": "es6"
+    },
+    "minify": false,
+    "isModule": true
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8148/input/input.js b/crates/swc/tests/fixture/issues-8xxx/8148/input/input.js
new file mode 100644
index 0000000000..6f7eddd979
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8148/input/input.js
@@ -0,0 +1,10 @@
+function _toBeMocked() {
+    return "I am the original function";
+}
+
+export let toBeMocked = _toBeMocked
+
+export const mock = {
+    get toBeMocked() { return toBeMocked; },
+    set toBeMocked(mock) { toBeMocked = mock; }
+}
\ No newline at end of file
diff --git a/crates/swc/tests/fixture/issues-8xxx/8148/output/input.js b/crates/swc/tests/fixture/issues-8xxx/8148/output/input.js
new file mode 100644
index 0000000000..56490d86fb
--- /dev/null
+++ b/crates/swc/tests/fixture/issues-8xxx/8148/output/input.js
@@ -0,0 +1,13 @@
+function _toBeMocked() {
+    return "I am the original function";
+}
+export var toBeMocked = _toBeMocked;
+var _$mock = {
+    get toBeMocked () {
+        return toBeMocked;
+    },
+    set toBeMocked (mock){
+        toBeMocked = mock;
+    }
+};
+export { _$mock as mock };
diff --git a/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-8148/1/input.js b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-8148/1/input.js
new file mode 100644
index 0000000000..1ce6ae8eaf
--- /dev/null
+++ b/crates/swc_ecma_transforms_compat/tests/block-scoping/issue-8148/1/input.js
@@ -0,0 +1,11 @@
+
+function _toBeMocked() {
+    return "I am the original function";
+}
+
+export let toBeMocked = _toBeMocked
+
+export const mock = {
+    get toBeMocked() { return toBeMocked; },
+    set toBeMocked(mock) { toBeMocked = mock; }
+}
\ No newline at end of file