diff --git a/crates/swc/tests/fixture/jest/mock-globals/input/.swcrc b/crates/swc/tests/fixture/jest/mock-globals/input/.swcrc new file mode 100644 index 000000000000..223146daeaf5 --- /dev/null +++ b/crates/swc/tests/fixture/jest/mock-globals/input/.swcrc @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true, + "dynamicImport": true + }, + "transform": { + "react": { + "runtime": "automatic" + } + }, + "baseUrl": "./", + "target": "es2021", + "keepClassNames": true + }, + "isModule": true, + "module": { + "type": "commonjs", + "ignoreDynamic": false + } +} \ No newline at end of file diff --git a/crates/swc/tests/fixture/jest/mock-globals/input/1.js b/crates/swc/tests/fixture/jest/mock-globals/input/1.js new file mode 100644 index 000000000000..6015aaa12718 --- /dev/null +++ b/crates/swc/tests/fixture/jest/mock-globals/input/1.js @@ -0,0 +1,17 @@ + +import { setTimeout } from 'timers/promises'; + +import { describe, expect, it, jest } from '@jest/globals'; + +jest.mock('timers/promises', () => ({ + setTimeout: jest.fn(() => Promise.resolve()) +})); + +describe('suite', () => { + it('my-test', () => { + const totalDelay = jest + .mocked(setTimeout) + .mock.calls.reduce((agg, call) => agg + (call[0] as number), 0); + expect(totalDelay).toStrictEqual(0); + }) +}) \ No newline at end of file diff --git a/crates/swc/tests/fixture/jest/mock-globals/output/1.js b/crates/swc/tests/fixture/jest/mock-globals/output/1.js new file mode 100644 index 000000000000..30ea46d51d08 --- /dev/null +++ b/crates/swc/tests/fixture/jest/mock-globals/output/1.js @@ -0,0 +1,15 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { + value: true +}); +const _promises = require("node:timers/promises"); +const _globals = require("@jest/globals"); +_globals.jest.mock('timers/promises', ()=>({ + setTimeout: _globals.jest.fn(()=>Promise.resolve()) + })); +(0, _globals.describe)('suite', ()=>{ + (0, _globals.it)('my-test', ()=>{ + const totalDelay = _globals.jest.mocked(_promises.setTimeout).mock.calls.reduce((agg, call)=>agg + call[0], 0); + (0, _globals.expect)(totalDelay).toStrictEqual(0); + }); +}); diff --git a/crates/swc_ecma_ast/src/module_decl.rs b/crates/swc_ecma_ast/src/module_decl.rs index d05454d46aa8..b77cfbc25f97 100644 --- a/crates/swc_ecma_ast/src/module_decl.rs +++ b/crates/swc_ecma_ast/src/module_decl.rs @@ -1,4 +1,5 @@ use is_macro::Is; +use swc_atoms::Atom; use swc_common::{ast_node, util::take::Take, EqIgnoreSpan, Span, DUMMY_SP}; use crate::{ @@ -313,3 +314,13 @@ pub enum ModuleExportName { #[tag("StringLiteral")] Str(Str), } + +impl ModuleExportName { + /// Get the atom of the export name. + pub fn atom(&self) -> &Atom { + match self { + ModuleExportName::Ident(i) => &i.sym, + ModuleExportName::Str(s) => &s.value, + } + } +} diff --git a/crates/swc_ecma_ext_transforms/src/jest.rs b/crates/swc_ecma_ext_transforms/src/jest.rs index 7d3bed7f8b9c..1c6eb13528db 100644 --- a/crates/swc_ecma_ext_transforms/src/jest.rs +++ b/crates/swc_ecma_ext_transforms/src/jest.rs @@ -13,10 +13,13 @@ static HOIST_METHODS: phf::Set<&str> = phf_set![ ]; pub fn jest() -> impl Fold + VisitMut { - as_folder(Jest) + as_folder(Jest::default()) } -struct Jest; +#[derive(Default)] +struct Jest { + imported: Vec, +} impl Jest { fn visit_mut_stmt_like(&mut self, orig: &mut Vec) @@ -38,21 +41,13 @@ impl Jest { Expr::Call(CallExpr { callee: Callee::Expr(callee), .. - }) => match &**callee { - Expr::Member( - callee @ MemberExpr { - prop: MemberProp::Ident(prop), - .. - }, - ) => { - if is_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym) { - hoisted.push(T::from_stmt(stmt)) - } else { - new.push(T::from_stmt(stmt)); - } + }) => { + if self.should_hoist(callee) { + hoisted.push(T::from_stmt(stmt)) + } else { + new.push(T::from_stmt(stmt)) } - _ => new.push(T::from_stmt(stmt)), - }, + } _ => new.push(T::from_stmt(stmt)), }, @@ -66,12 +61,63 @@ impl Jest { *orig = new; } + + fn should_hoist(&self, e: &Expr) -> bool { + match e { + Expr::Ident(i) => self.imported.iter().any(|imported| *imported == i.to_id()), + + Expr::Member( + callee @ MemberExpr { + prop: MemberProp::Ident(prop), + .. + }, + ) => is_global_jest(&callee.obj) && HOIST_METHODS.contains(&*prop.sym), + + _ => false, + } + } } impl VisitMut for Jest { noop_visit_mut_type!(); fn visit_mut_module_items(&mut self, items: &mut Vec) { + for item in items.iter() { + if let ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { + specifiers, src, .. + })) = item + { + if src.value == "@jest/globals" { + for s in specifiers { + match s { + ImportSpecifier::Named(ImportNamedSpecifier { + local, + imported: None, + is_type_only: false, + .. + }) => { + if HOIST_METHODS.contains(&*local.sym) { + self.imported.push(local.to_id()); + } + } + + ImportSpecifier::Named(ImportNamedSpecifier { + local, + imported: Some(exported), + is_type_only: false, + .. + }) => { + if HOIST_METHODS.contains(exported.atom()) { + self.imported.push(local.to_id()); + } + } + _ => {} + } + } + } + } + } + self.visit_mut_stmt_like(items) } @@ -80,14 +126,14 @@ impl VisitMut for Jest { } } -fn is_jest(e: &Expr) -> bool { +fn is_global_jest(e: &Expr) -> bool { match e { Expr::Ident(i) => i.sym == *"jest", - Expr::Member(MemberExpr { obj, .. }) => is_jest(obj), + Expr::Member(MemberExpr { obj, .. }) => is_global_jest(obj), Expr::Call(CallExpr { callee: Callee::Expr(callee), .. - }) => is_jest(callee), + }) => is_global_jest(callee), _ => false, } }