Repository: swc-project/swc (rust)

Issue:
jest.spyOn does not work when compared ts-jest
## Problem

- Doesn't work test when another function is called inside function.
- But this works with ts-jest.
- I understand that can do the same by injecting it as a callback function.

### index.ts

```ts
export const child = () => {
  console.log('Hello World!');
};

export const callChild = () => {
  child();
};
```

### index.spec.ts

```ts
import * as index from '.';

describe('index', () => {
  it('called', () => {
    const spiedChild = jest.spyOn(index, 'child');
    index.callChild();

    expect(spiedChild).toHaveBeenCalled();
  });
});
```

### ts-jest

#### jest.config.js

```js
module.exports = {
  clearMocks: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8',
  preset: 'ts-jest',
  clearMocks: true,
  testEnvironment: 'node',
  roots: ['<rootDir>/src/'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: ['src/**/*.{ts,js}'],
  testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|dist)[/\\\\]'],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  silent: false,
};
```

#### Result

```
> jest

  console.log
    Hello World!

      at child (src/index.ts:4:11)

 PASS  src/index.spec.ts
  index
    √ called (29 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        2.603 s, estimated 3 s
Ran all test suites.
```

### @swc/jest

#### jest.swc.config.js

```js
module.exports = {
  clearMocks: true,
  coverageDirectory: 'coverage',
  coverageProvider: 'v8',
  transform: {
    '^.+\\.(t|j)sx?$': ['@swc/jest'],
  },
  clearMocks: true,
  testEnvironment: 'node',
  roots: ['<rootDir>/src/'],
  moduleFileExtensions: ['ts', 'js'],
  collectCoverageFrom: ['src/**/*.{ts,js}'],
  testPathIgnorePatterns: ['<rootDir>[/\\\\](node_modules|dist)[/\\\\]'],
  watchPlugins: [
    'jest-watch-typeahead/filename',
    'jest-watch-typeahead/testname',
  ],
  silent: false,
  extensionsToTreatAsEsm: ['.ts', '.tsx'],
};
```

#### Result

```
> jest -c jest.swc.config.js

  console.log
    Hello World!

      at child (src/index.ts:7:13)

 FAIL  src/index.spec.ts
  index
    × called (22 ms)

  ● index › called

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0



      at Object.<anonymous> (src/index.spec.ts:30:28)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        0.408 s, estimated 1 s
Ran all test suites.
```


Pull request:
fix(es/module): Fix `jest.spyOn`
**Description:**



**Related issue (if exists):**


 - Closes #3843

Patch:
diff --git a/crates/swc_ecma_transforms_module/src/common_js.rs b/crates/swc_ecma_transforms_module/src/common_js.rs
index d701403b82..b28969db28 100644
--- a/crates/swc_ecma_transforms_module/src/common_js.rs
+++ b/crates/swc_ecma_transforms_module/src/common_js.rs
@@ -12,8 +12,8 @@ use swc_common::{
 use swc_ecma_ast::*;
 use swc_ecma_transforms_base::helper;
 use swc_ecma_utils::{
-    ident::IdentLike, member_expr, private_ident, quote_ident, quote_str, var::VarCollector,
-    DestructuringFinder, ExprFactory, IsDirective,
+    find_ids, ident::IdentLike, member_expr, private_ident, quote_ident, quote_str,
+    var::VarCollector, DestructuringFinder, ExprFactory, IsDirective,
 };
 use swc_ecma_visit::{noop_fold_type, noop_visit_type, Fold, FoldWith, Visit, VisitWith};
 
@@ -193,6 +193,19 @@ impl Fold for CommonJs {
             }
         }
 
+        for item in &items {
+            if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(ExportDecl {
+                decl: Decl::Var(v),
+                ..
+            })) = item
+            {
+                self.scope
+                    .borrow_mut()
+                    .exported_var_decls
+                    .extend(find_ids(&v.decls));
+            }
+        }
+
         // Make another preliminary pass to collect all import sources and their
         // specifiers.
         for item in &items {
@@ -386,9 +399,9 @@ impl Fold for CommonJs {
                             decl: Decl::Var(var),
                             ..
                         })) => {
-                            extra_stmts.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(
-                                var.clone().fold_with(self),
-                            ))));
+                            let new_var = var.clone().fold_with(self);
+
+                            extra_stmts.push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(new_var))));
 
                             let mut scope = self.scope.borrow_mut();
                             var.decls.visit_with(&mut VarCollector {
diff --git a/crates/swc_ecma_transforms_module/src/util.rs b/crates/swc_ecma_transforms_module/src/util.rs
index a62ed4a6aa..5b30ad748d 100644
--- a/crates/swc_ecma_transforms_module/src/util.rs
+++ b/crates/swc_ecma_transforms_module/src/util.rs
@@ -11,7 +11,7 @@ use swc_atoms::{js_word, JsWord};
 use swc_common::{
     collections::{AHashMap, AHashSet},
     util::take::Take,
-    Span, DUMMY_SP,
+    Span, SyntaxContext, DUMMY_SP,
 };
 use swc_ecma_ast::*;
 use swc_ecma_utils::{
@@ -117,7 +117,7 @@ pub struct Scope {
     ///   -> `{foo: ('bar', default)}`
     pub(crate) idents: AHashMap<Id, (JsWord, JsWord)>,
 
-    /// Declared variables except const.
+    /// Declared variables.
     pub(crate) declared_vars: Vec<Id>,
 
     /// Maps of exported bindings.
@@ -131,9 +131,13 @@ pub struct Scope {
     ///   -> `{ a: [b] }`
     pub(crate) exported_bindings: AHashMap<Id, Vec<Id>>,
 
+    pub(crate) exported_var_decls: AHashSet<Id>,
+
     /// This is required to handle
     /// `export * from 'foo';`
     pub(crate) lazy_blacklist: AHashSet<JsWord>,
+
+    exports: Option<Ident>,
 }
 
 impl Scope {
@@ -422,13 +426,28 @@ impl Scope {
         match value {
             Ok(value) => Prop::KeyValue(KeyValueProp {
                 key: PropName::Ident(key),
-                value: Box::new(value),
+                value: Box::new(value.0),
             }),
             Err(ident) => Prop::Shorthand(ident),
         }
     }
 
-    fn fold_ident(folder: &mut impl ModulePass, i: Ident) -> Result<Expr, Ident> {
+    /// Try converting identifier to member expression referring imports.
+    ///
+    /// returns `(expr, true)` for import refs and `(expr, false)` for exports.
+    fn fold_ident(folder: &mut impl ModulePass, i: Ident) -> Result<(Expr, bool), Ident> {
+        if folder.scope().exported_var_decls.contains(&i.to_id()) {
+            return Ok((
+                folder
+                    .scope()
+                    .exports
+                    .clone()
+                    .unwrap()
+                    .make_member(Ident::new(i.sym, i.span.with_ctxt(SyntaxContext::empty()))),
+                false,
+            ));
+        }
+
         let orig_span = i.span;
         let v = folder.scope().idents.get(&i.to_id()).cloned();
         match v {
@@ -460,9 +479,9 @@ impl Scope {
 
                 if *prop == js_word!("") {
                     // import * as foo from 'foo';
-                    Ok(obj)
+                    Ok((obj, true))
                 } else {
-                    Ok(obj.make_member(Ident::new(prop, DUMMY_SP)))
+                    Ok((obj.make_member(Ident::new(prop, DUMMY_SP)), true))
                 }
             }
         }
@@ -474,6 +493,8 @@ impl Scope {
         top_level: bool,
         expr: Expr,
     ) -> Expr {
+        folder.scope_mut().exports = Some(exports.clone());
+
         macro_rules! chain_assign {
             ($entry:expr, $e:expr) => {{
                 let mut e = $e;
@@ -497,7 +518,7 @@ impl Scope {
             // In a JavaScript module, this is undefined at the top level (i.e., outside functions).
             Expr::This(ThisExpr { span }) if top_level => *undefined(span),
             Expr::Ident(i) => match Self::fold_ident(folder, i) {
-                Ok(expr) => expr,
+                Ok(expr) => expr.0,
                 Err(ident) => Expr::Ident(ident),
             },
 
@@ -515,7 +536,7 @@ impl Scope {
                 let expr = args.pop().unwrap().expr.fold_with(folder);
                 let expr = match *expr {
                     Expr::Ident(ident) => match Self::fold_ident(folder, ident) {
-                        Ok(expr) => expr,
+                        Ok(expr) => expr.0,
                         Err(ident) => Expr::Ident(ident),
                     },
                     expr => expr,
@@ -533,21 +554,24 @@ impl Scope {
                 let callee = if let Callee::Expr(expr) = callee {
                     let callee = if let Expr::Ident(ident) = *expr {
                         match Self::fold_ident(folder, ident) {
-                            Ok(mut expr) => {
-                                if let Expr::Member(member) = &mut expr {
-                                    if let Expr::Ident(ident) = member.obj.as_mut() {
-                                        member.obj = Box::new(Expr::Paren(ParenExpr {
-                                            expr: Box::new(Expr::Seq(SeqExpr {
+                            Ok((mut expr, need_zero)) => {
+                                if need_zero {
+                                    if let Expr::Member(member) = &mut expr {
+                                        if let Expr::Ident(ident) = member.obj.as_mut() {
+                                            member.obj = Box::new(Expr::Paren(ParenExpr {
+                                                expr: Box::new(Expr::Seq(SeqExpr {
+                                                    span,
+                                                    exprs: vec![
+                                                        Box::new(0_f64.into()),
+                                                        Box::new(ident.take().into()),
+                                                    ],
+                                                })),
                                                 span,
-                                                exprs: vec![
-                                                    Box::new(0_f64.into()),
-                                                    Box::new(ident.take().into()),
-                                                ],
-                                            })),
-                                            span,
-                                        }))
-                                    }
-                                };
+                                            }))
+                                        }
+                                    };
+                                }
+
                                 expr
                             }
                             Err(ident) => Expr::Ident(ident),
@@ -707,7 +731,7 @@ impl Scope {
                         let left = if let PatOrExpr::Pat(ref left_pat) = expr.left {
                             if let Pat::Ident(BindingIdent { ref id, .. }) = **left_pat {
                                 let expr = match Self::fold_ident(folder, id.clone()) {
-                                    Ok(expr) => expr,
+                                    Ok(expr) => expr.0,
                                     Err(ident) => Expr::Ident(ident),
                                 };
                                 PatOrExpr::Expr(Box::new(expr))


Test patch:
diff --git a/crates/swc/tests/fixture/issue-2606/1/output/index.ts b/crates/swc/tests/fixture/issue-2606/1/output/index.ts
index 76249b3624..f21112289c 100644
--- a/crates/swc/tests/fixture/issue-2606/1/output/index.ts
+++ b/crates/swc/tests/fixture/issue-2606/1/output/index.ts
@@ -11,14 +11,14 @@ const test = {};
 exports.test = test;
 const test2 = {};
 exports.test2 = test2;
-Object.defineProperty(test, "warn", {
+Object.defineProperty(exports.test, "warn", {
     get: ()=>warn
     ,
     set: (v)=>{
         exports.warn = warn = v;
     }
 });
-Object.defineProperty(test2, "work", {
+Object.defineProperty(exports.test2, "work", {
     get: ()=>warn
     ,
     set: (v)=>{
diff --git a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2549/output.js b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2549/output.js
index 26692b536d..e72fda5a0a 100644
--- a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2549/output.js
+++ b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2549/output.js
@@ -21,13 +21,12 @@ exports.errors = errors;
 const addOne = (x)=>`${x + 1}`
 ;
 exports.addOne = addOne;
-const someFunc = (x)=>`The answer is : ${addOne(x)}`
+const someFunc = (x)=>`The answer is : ${exports.addOne(x)}`
 ;
 exports.someFunc = someFunc;
-const test = {
-};
+const test = {};
 exports.test = test;
-Object.defineProperty(test, "log", {
+Object.defineProperty(exports.test, "log", {
     get: function get() {
         return log;
     },
@@ -35,15 +34,15 @@ Object.defineProperty(test, "log", {
         log = v;
     }
 });
-Object.defineProperty(test, "warn", {
+Object.defineProperty(exports.test, "warn", {
     get: ()=>warn
     ,
     set: (v)=>{
         exports.warn = warn = v;
     }
 });
-Object.defineProperty(test, "errors", {
-    get: ()=>errors
+Object.defineProperty(exports.test, "errors", {
+    get: ()=>exports.errors
     ,
     set: (v)=>{
         exports.errors = errors = v;
diff --git a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2678/output.js b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2678/output.js
index e9b9ddc998..f27465abf3 100644
--- a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2678/output.js
+++ b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-2678/output.js
@@ -11,15 +11,14 @@ function someCall() {
 function warn() {
     throw new Error("this should not be called");
 }
-const test = {
-};
+const test = {};
 exports.test = test;
-Object.defineProperty(test, "someCall", {
+Object.defineProperty(exports.test, "someCall", {
     set: (v)=>{
         exports.default = someCall = v;
     }
 });
-Object.defineProperty(test, "warn", {
+Object.defineProperty(exports.test, "warn", {
     get: ()=>warn
     ,
     set: (v)=>{
diff --git a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/input.js b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/input.js
new file mode 100644
index 0000000000..394a93b729
--- /dev/null
+++ b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/input.js
@@ -0,0 +1,7 @@
+export const child = () => {
+    console.log('Hello World!');
+};
+
+export const callChild = () => {
+    child();
+};
\ No newline at end of file
diff --git a/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/output.js b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/output.js
new file mode 100644
index 0000000000..c0ec616910
--- /dev/null
+++ b/crates/swc_ecma_transforms_module/tests/fixture/commonjs/issue-3843/1/output.js
@@ -0,0 +1,13 @@
+"use strict";
+Object.defineProperty(exports, "__esModule", {
+    value: true
+});
+exports.callChild = exports.child = void 0;
+const child = ()=>{
+    console.log("Hello World!");
+};
+exports.child = child;
+const callChild = ()=>{
+    exports.child();
+};
+exports.callChild = callChild;