swc_ecma_transforms_testing/
lib.rs

1#![deny(clippy::all)]
2#![deny(clippy::all)]
3#![deny(unused)]
4#![allow(clippy::result_unit_err)]
5
6use std::{
7    env,
8    fs::{self, create_dir_all, read_to_string, OpenOptions},
9    io::Write,
10    mem::{take, transmute},
11    panic,
12    path::{Path, PathBuf},
13    process::Command,
14    rc::Rc,
15};
16
17use ansi_term::Color;
18use anyhow::Error;
19use base64::prelude::{Engine, BASE64_STANDARD};
20use serde::de::DeserializeOwned;
21use sha2::{Digest, Sha256};
22use swc_common::{
23    comments::{Comments, SingleThreadedComments},
24    errors::{Handler, HANDLER},
25    source_map::SourceMapGenConfig,
26    sync::Lrc,
27    FileName, Mark, SourceMap, DUMMY_SP,
28};
29use swc_ecma_ast::*;
30use swc_ecma_codegen::{to_code_default, Emitter};
31use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax};
32use swc_ecma_testing::{exec_node_js, JsExecOptions};
33use swc_ecma_transforms_base::{
34    fixer,
35    helpers::{inject_helpers, HELPERS},
36    hygiene,
37};
38use swc_ecma_utils::{quote_ident, quote_str, ExprFactory};
39use swc_ecma_visit::{noop_visit_mut_type, visit_mut_pass, Fold, FoldWith, VisitMut};
40use tempfile::tempdir_in;
41use testing::{
42    assert_eq, find_executable, NormalizedOutput, CARGO_TARGET_DIR, CARGO_WORKSPACE_ROOT,
43};
44
45pub mod babel_like;
46
47pub struct Tester<'a> {
48    pub cm: Lrc<SourceMap>,
49    pub handler: &'a Handler,
50    /// This will be changed to [SingleThreadedComments] once `cargo-mono`
51    /// supports correct bumping logic, or we need to make a breaking change in
52    /// a upstream crate of this crate.
53    ///
54    /// Although type is `Rc<SingleThreadedComments>`, it's fine to clone
55    /// `SingleThreadedComments` without `Rc`.
56    pub comments: Rc<SingleThreadedComments>,
57}
58
59impl Tester<'_> {
60    pub fn run<F, Ret>(op: F) -> Ret
61    where
62        F: FnOnce(&mut Tester<'_>) -> Result<Ret, ()>,
63    {
64        let comments = Rc::new(SingleThreadedComments::default());
65
66        let out = ::testing::run_test(false, |cm, handler| {
67            HANDLER.set(handler, || {
68                HELPERS.set(&Default::default(), || {
69                    let cmts = comments.clone();
70                    let c = Box::new(unsafe {
71                        // Safety: This is unsafe but it's used only for testing.
72                        transmute::<&dyn Comments, &'static dyn Comments>(&*cmts)
73                    }) as Box<dyn Comments>;
74                    swc_common::comments::COMMENTS.set(&c, || {
75                        op(&mut Tester {
76                            cm,
77                            handler,
78                            comments,
79                        })
80                    })
81                })
82            })
83        });
84
85        match out {
86            Ok(ret) => ret,
87            Err(stderr) => panic!("Stderr:\n{stderr}"),
88        }
89    }
90
91    pub(crate) fn run_captured<F, T>(op: F) -> (Option<T>, NormalizedOutput)
92    where
93        F: FnOnce(&mut Tester<'_>) -> Result<T, ()>,
94    {
95        let mut res = None;
96        let output = ::testing::Tester::new().print_errors(|cm, handler| -> Result<(), _> {
97            HANDLER.set(&handler, || {
98                HELPERS.set(&Default::default(), || {
99                    let result = op(&mut Tester {
100                        cm,
101                        handler: &handler,
102                        comments: Default::default(),
103                    });
104
105                    res = result.ok();
106
107                    // We need stderr
108                    Err(())
109                })
110            })
111        });
112
113        let output = output
114            .err()
115            .unwrap_or_else(|| NormalizedOutput::from(String::from("")));
116
117        (res, output)
118    }
119
120    pub fn with_parser<F, T>(
121        &mut self,
122        file_name: &str,
123        syntax: Syntax,
124        src: &str,
125        op: F,
126    ) -> Result<T, ()>
127    where
128        F: FnOnce(&mut Parser<Lexer>) -> Result<T, swc_ecma_parser::error::Error>,
129    {
130        let fm = self
131            .cm
132            .new_source_file(FileName::Real(file_name.into()).into(), src.to_string());
133
134        let mut p = Parser::new(syntax, StringInput::from(&*fm), Some(&self.comments));
135        let res = op(&mut p).map_err(|e| e.into_diagnostic(self.handler).emit());
136
137        for e in p.take_errors() {
138            e.into_diagnostic(self.handler).emit()
139        }
140
141        res
142    }
143
144    pub fn parse_module(&mut self, file_name: &str, src: &str) -> Result<Module, ()> {
145        self.with_parser(file_name, Syntax::default(), src, |p| p.parse_module())
146    }
147
148    pub fn parse_stmts(&mut self, file_name: &str, src: &str) -> Result<Vec<Stmt>, ()> {
149        let stmts = self.with_parser(file_name, Syntax::default(), src, |p| {
150            p.parse_script().map(|script| script.body)
151        })?;
152
153        Ok(stmts)
154    }
155
156    pub fn parse_stmt(&mut self, file_name: &str, src: &str) -> Result<Stmt, ()> {
157        let mut stmts = self.parse_stmts(file_name, src)?;
158        assert!(stmts.len() == 1);
159
160        Ok(stmts.pop().unwrap())
161    }
162
163    pub fn apply_transform<T: Pass>(
164        &mut self,
165        tr: T,
166        name: &str,
167        syntax: Syntax,
168        is_module: Option<bool>,
169        src: &str,
170    ) -> Result<Program, ()> {
171        let program =
172            self.with_parser(
173                name,
174                syntax,
175                src,
176                |parser: &mut Parser<Lexer>| match is_module {
177                    Some(true) => parser.parse_module().map(Program::Module),
178                    Some(false) => parser.parse_script().map(Program::Script),
179                    None => parser.parse_program(),
180                },
181            )?;
182
183        Ok(program.apply(tr))
184    }
185
186    pub fn print(&mut self, program: &Program, comments: &Rc<SingleThreadedComments>) -> String {
187        to_code_default(self.cm.clone(), Some(comments), program)
188    }
189}
190
191struct RegeneratorHandler;
192
193impl VisitMut for RegeneratorHandler {
194    noop_visit_mut_type!();
195
196    fn visit_mut_module_item(&mut self, item: &mut ModuleItem) {
197        if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
198            if &*import.src.value != "regenerator-runtime" {
199                return;
200            }
201
202            let s = import.specifiers.iter().find_map(|v| match v {
203                ImportSpecifier::Default(rt) => Some(rt.local.clone()),
204                _ => None,
205            });
206
207            let s = match s {
208                Some(v) => v,
209                _ => return,
210            };
211
212            let init = CallExpr {
213                span: DUMMY_SP,
214                callee: quote_ident!("require").as_callee(),
215                args: vec![quote_str!("regenerator-runtime").as_arg()],
216                ..Default::default()
217            }
218            .into();
219
220            let decl = VarDeclarator {
221                span: DUMMY_SP,
222                name: s.into(),
223                init: Some(init),
224                definite: Default::default(),
225            };
226            *item = VarDecl {
227                span: import.span,
228                kind: VarDeclKind::Var,
229                declare: false,
230                decls: vec![decl],
231                ..Default::default()
232            }
233            .into()
234        }
235    }
236}
237
238#[track_caller]
239pub fn test_transform<F, P>(
240    syntax: Syntax,
241    is_module: Option<bool>,
242    tr: F,
243    input: &str,
244    expected: &str,
245) where
246    F: FnOnce(&mut Tester) -> P,
247    P: Pass,
248{
249    Tester::run(|tester| {
250        let expected = tester.apply_transform(
251            swc_ecma_utils::DropSpan,
252            "output.js",
253            syntax,
254            is_module,
255            expected,
256        )?;
257
258        let expected_comments = take(&mut tester.comments);
259
260        println!("----- Actual -----");
261
262        let tr = (tr(tester), visit_mut_pass(RegeneratorHandler));
263        let actual = tester.apply_transform(tr, "input.js", syntax, is_module, input)?;
264
265        match ::std::env::var("PRINT_HYGIENE") {
266            Ok(ref s) if s == "1" => {
267                let hygiene_src = tester.print(
268                    &actual.clone().fold_with(&mut HygieneVisualizer),
269                    &tester.comments.clone(),
270                );
271                println!("----- Hygiene -----\n{hygiene_src}");
272            }
273            _ => {}
274        }
275
276        let actual = actual
277            .apply(::swc_ecma_utils::DropSpan)
278            .apply(hygiene::hygiene())
279            .apply(fixer::fixer(Some(&tester.comments)));
280
281        println!("{:?}", tester.comments);
282        println!("{expected_comments:?}");
283
284        {
285            let (actual_leading, actual_trailing) = tester.comments.borrow_all();
286            let (expected_leading, expected_trailing) = expected_comments.borrow_all();
287
288            if actual == expected
289                && *actual_leading == *expected_leading
290                && *actual_trailing == *expected_trailing
291            {
292                return Ok(());
293            }
294        }
295
296        let (actual_src, expected_src) = (
297            tester.print(&actual, &tester.comments.clone()),
298            tester.print(&expected, &expected_comments),
299        );
300
301        if actual_src == expected_src {
302            return Ok(());
303        }
304
305        println!(">>>>> {} <<<<<\n{}", Color::Green.paint("Orig"), input);
306        println!(">>>>> {} <<<<<\n{}", Color::Green.paint("Code"), actual_src);
307
308        if actual_src != expected_src {
309            panic!(
310                r#"assertion failed: `(left == right)`
311            {}"#,
312                ::testing::diff(&actual_src, &expected_src),
313            );
314        }
315
316        Err(())
317    });
318}
319
320/// NOT A PUBLIC API. DO NOT USE.
321#[doc(hidden)]
322#[track_caller]
323pub fn test_inline_input_output<F, P>(
324    syntax: Syntax,
325    is_module: Option<bool>,
326    tr: F,
327    input: &str,
328    output: &str,
329) where
330    F: FnOnce(&mut Tester) -> P,
331    P: Pass,
332{
333    let _logger = testing::init();
334
335    let expected = output;
336
337    let expected_src = Tester::run(|tester| {
338        let expected_program =
339            tester.apply_transform(noop_pass(), "expected.js", syntax, is_module, expected)?;
340
341        let expected_src = tester.print(&expected_program, &Default::default());
342
343        println!(
344            "----- {} -----\n{}",
345            Color::Green.paint("Expected"),
346            expected_src
347        );
348
349        Ok(expected_src)
350    });
351
352    let actual_src = Tester::run_captured(|tester| {
353        println!("----- {} -----\n{}", Color::Green.paint("Input"), input);
354
355        let tr = tr(tester);
356
357        println!("----- {} -----", Color::Green.paint("Actual"));
358
359        let actual = tester.apply_transform(tr, "input.js", syntax, is_module, input)?;
360
361        match ::std::env::var("PRINT_HYGIENE") {
362            Ok(ref s) if s == "1" => {
363                let hygiene_src = tester.print(
364                    &actual.clone().fold_with(&mut HygieneVisualizer),
365                    &Default::default(),
366                );
367                println!(
368                    "----- {} -----\n{}",
369                    Color::Green.paint("Hygiene"),
370                    hygiene_src
371                );
372            }
373            _ => {}
374        }
375
376        let actual = actual
377            .apply(crate::hygiene::hygiene())
378            .apply(crate::fixer::fixer(Some(&tester.comments)));
379
380        let actual_src = tester.print(&actual, &Default::default());
381
382        Ok(actual_src)
383    })
384    .0
385    .unwrap();
386
387    assert_eq!(
388        expected_src, actual_src,
389        "Exepcted:\n{expected_src}\nActual:\n{actual_src}\n",
390    );
391}
392
393/// NOT A PUBLIC API. DO NOT USE.
394#[doc(hidden)]
395#[track_caller]
396pub fn test_inlined_transform<F, P>(
397    test_name: &str,
398    syntax: Syntax,
399    module: Option<bool>,
400    tr: F,
401    input: &str,
402) where
403    F: FnOnce(&mut Tester) -> P,
404    P: Pass,
405{
406    let loc = panic::Location::caller();
407
408    let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR"));
409
410    let test_file_path = CARGO_WORKSPACE_ROOT.join(loc.file());
411
412    let snapshot_dir = manifest_dir.join("tests").join("__swc_snapshots__").join(
413        test_file_path
414            .strip_prefix(&manifest_dir)
415            .expect("test_inlined_transform does not support paths outside of the crate root"),
416    );
417
418    test_fixture_inner(
419        syntax,
420        Box::new(move |tester| Box::new(tr(tester))),
421        input,
422        &snapshot_dir.join(format!("{test_name}.js")),
423        FixtureTestConfig {
424            module,
425            ..Default::default()
426        },
427    )
428}
429
430/// NOT A PUBLIC API. DO NOT USE.
431#[doc(hidden)]
432#[macro_export]
433macro_rules! test_location {
434    () => {{
435        $crate::TestLocation {}
436    }};
437}
438
439#[macro_export]
440macro_rules! test_inline {
441    (ignore, $syntax:expr, $tr:expr, $test_name:ident, $input:expr, $output:expr) => {
442        #[test]
443        #[ignore]
444        fn $test_name() {
445            $crate::test_inline_input_output($syntax, None, $tr, $input, $output)
446        }
447    };
448
449    ($syntax:expr, $tr:expr, $test_name:ident, $input:expr, $output:expr) => {
450        #[test]
451        fn $test_name() {
452            $crate::test_inline_input_output($syntax, None, $tr, $input, $output)
453        }
454    };
455}
456
457test_inline!(
458    ignore,
459    Syntax::default(),
460    |_| noop_pass(),
461    test_inline_ignored,
462    "class Foo {}",
463    "class Foo {}"
464);
465
466test_inline!(
467    Syntax::default(),
468    |_| noop_pass(),
469    test_inline_pass,
470    "class Foo {}",
471    "class Foo {}"
472);
473
474#[test]
475#[should_panic]
476fn test_inline_should_fail() {
477    test_inline_input_output(
478        Default::default(),
479        None,
480        |_| noop_pass(),
481        "class Foo {}",
482        "",
483    );
484}
485
486#[macro_export]
487macro_rules! test {
488    (ignore, $syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
489        #[test]
490        #[ignore]
491        fn $test_name() {
492            $crate::test_inlined_transform(stringify!($test_name), $syntax, None, $tr, $input)
493        }
494    };
495
496    ($syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
497        #[test]
498        fn $test_name() {
499            $crate::test_inlined_transform(stringify!($test_name), $syntax, None, $tr, $input)
500        }
501    };
502
503    (module, $syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
504        #[test]
505        fn $test_name() {
506            $crate::test_inlined_transform(stringify!($test_name), $syntax, Some(true), $tr, $input)
507        }
508    };
509
510    (script, $syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
511        #[test]
512        fn $test_name() {
513            $crate::test_inlined_script_transform(
514                stringify!($test_name),
515                $syntax,
516                Some(false),
517                $tr,
518                $input,
519            )
520        }
521    };
522
523    ($syntax:expr, $tr:expr, $test_name:ident, $input:expr, ok_if_code_eq) => {
524        #[test]
525        fn $test_name() {
526            $crate::test_inlined_transform(stringify!($test_name), $syntax, None, $tr, $input)
527        }
528    };
529}
530
531/// Execute `node` for `input` and ensure that it prints same output after
532/// transformation.
533pub fn compare_stdout<F, P>(syntax: Syntax, tr: F, input: &str)
534where
535    F: FnOnce(&mut Tester<'_>) -> P,
536    P: Pass,
537{
538    Tester::run(|tester| {
539        let tr = (tr(tester), visit_mut_pass(RegeneratorHandler));
540
541        let program = tester.apply_transform(tr, "input.js", syntax, Some(true), input)?;
542
543        match ::std::env::var("PRINT_HYGIENE") {
544            Ok(ref s) if s == "1" => {
545                let hygiene_src = tester.print(
546                    &program.clone().fold_with(&mut HygieneVisualizer),
547                    &tester.comments.clone(),
548                );
549                println!("----- Hygiene -----\n{hygiene_src}");
550            }
551            _ => {}
552        }
553
554        let mut program = program
555            .apply(hygiene::hygiene())
556            .apply(fixer::fixer(Some(&tester.comments)));
557
558        let src_without_helpers = tester.print(&program, &tester.comments.clone());
559        program = program.apply(inject_helpers(Mark::fresh(Mark::root())));
560
561        let transformed_src = tester.print(&program, &tester.comments.clone());
562
563        println!("\t>>>>> Orig <<<<<\n{input}\n\t>>>>> Code <<<<<\n{src_without_helpers}");
564
565        let expected = stdout_of(input).unwrap();
566
567        println!("\t>>>>> Expected stdout <<<<<\n{expected}");
568
569        let actual = stdout_of(&transformed_src).unwrap();
570
571        assert_eq!(expected, actual);
572
573        Ok(())
574    })
575}
576
577/// Execute `jest` after transpiling `input` using `tr`.
578pub fn exec_tr<F, P>(_test_name: &str, syntax: Syntax, tr: F, input: &str)
579where
580    F: FnOnce(&mut Tester<'_>) -> P,
581    P: Pass,
582{
583    Tester::run(|tester| {
584        let tr = (tr(tester), visit_mut_pass(RegeneratorHandler));
585
586        let program = tester.apply_transform(
587            tr,
588            "input.js",
589            syntax,
590            Some(true),
591            &format!(
592                "it('should work', async function () {{
593                    {input}
594                }})"
595            ),
596        )?;
597        match ::std::env::var("PRINT_HYGIENE") {
598            Ok(ref s) if s == "1" => {
599                let hygiene_src = tester.print(
600                    &program.clone().fold_with(&mut HygieneVisualizer),
601                    &tester.comments.clone(),
602                );
603                println!("----- Hygiene -----\n{hygiene_src}");
604            }
605            _ => {}
606        }
607
608        let mut program = program
609            .apply(hygiene::hygiene())
610            .apply(fixer::fixer(Some(&tester.comments)));
611
612        let src_without_helpers = tester.print(&program, &tester.comments.clone());
613        program = program.apply(inject_helpers(Mark::fresh(Mark::root())));
614
615        let src = tester.print(&program, &tester.comments.clone());
616
617        println!(
618            "\t>>>>> {} <<<<<\n{}\n\t>>>>> {} <<<<<\n{}",
619            Color::Green.paint("Orig"),
620            input,
621            Color::Green.paint("Code"),
622            src_without_helpers
623        );
624
625        exec_with_node_test_runner(&src).map(|_| {})
626    })
627}
628
629fn calc_hash(s: &str) -> String {
630    let mut hasher = Sha256::new();
631    hasher.update(s.as_bytes());
632    let sum = hasher.finalize();
633
634    hex::encode(sum)
635}
636
637fn exec_with_node_test_runner(src: &str) -> Result<(), ()> {
638    let root = CARGO_TARGET_DIR.join("swc-es-exec-testing");
639
640    create_dir_all(&root).expect("failed to create parent directory for temp directory");
641
642    let hash = calc_hash(src);
643    let success_cache = root.join(format!("{hash}.success"));
644
645    if env::var("SWC_CACHE_TEST").unwrap_or_default() == "1" {
646        println!("Trying cache as `SWC_CACHE_TEST` is `1`");
647
648        if success_cache.exists() {
649            println!("Cache: success");
650            return Ok(());
651        }
652    }
653
654    let tmp_dir = tempdir_in(&root).expect("failed to create a temp directory");
655    create_dir_all(&tmp_dir).unwrap();
656
657    let path = tmp_dir.path().join(format!("{hash}.test.js"));
658
659    let mut tmp = OpenOptions::new()
660        .create(true)
661        .truncate(true)
662        .write(true)
663        .open(&path)
664        .expect("failed to create a temp file");
665    write!(tmp, "{src}").expect("failed to write to temp file");
666    tmp.flush().unwrap();
667
668    let test_runner_path = find_executable("mocha").expect("failed to find `mocha` from path");
669
670    let mut base_cmd = if cfg!(target_os = "windows") {
671        let mut c = Command::new("cmd");
672        c.arg("/C").arg(&test_runner_path);
673        c
674    } else {
675        Command::new(&test_runner_path)
676    };
677
678    let output = base_cmd
679        .arg(format!("{}", path.display()))
680        .arg("--color")
681        .current_dir(root)
682        .output()
683        .expect("failed to run mocha");
684
685    println!(">>>>> {} <<<<<", Color::Red.paint("Stdout"));
686    println!("{}", String::from_utf8_lossy(&output.stdout));
687    println!(">>>>> {} <<<<<", Color::Red.paint("Stderr"));
688    println!("{}", String::from_utf8_lossy(&output.stderr));
689
690    if output.status.success() {
691        fs::write(&success_cache, "").unwrap();
692        return Ok(());
693    }
694    let dir_name = path.display().to_string();
695    ::std::mem::forget(tmp_dir);
696    panic!("Execution failed: {dir_name}")
697}
698
699fn stdout_of(code: &str) -> Result<String, Error> {
700    exec_node_js(
701        code,
702        JsExecOptions {
703            cache: true,
704            module: false,
705            ..Default::default()
706        },
707    )
708}
709
710/// Test transformation.
711#[macro_export]
712macro_rules! test_exec {
713    (@check) => {
714        if ::std::env::var("EXEC").unwrap_or(String::from("")) == "0" {
715            return;
716        }
717    };
718
719    (ignore, $syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
720        #[test]
721        #[ignore]
722        fn $test_name() {
723            $crate::exec_tr(stringify!($test_name), $syntax, $tr, $input)
724        }
725    };
726
727    ($syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
728        #[test]
729        fn $test_name() {
730            test_exec!(@check);
731            $crate::exec_tr(stringify!($test_name), $syntax, $tr, $input)
732        }
733    };
734}
735
736/// Test transformation by invoking it using `node`. The code must print
737/// something to stdout.
738#[macro_export]
739macro_rules! compare_stdout {
740    ($syntax:expr, $tr:expr, $test_name:ident, $input:expr) => {
741        #[test]
742        fn $test_name() {
743            $crate::compare_stdout($syntax, $tr, $input)
744        }
745    };
746}
747
748/// Converts `foo#1` to `foo__1` so it can be verified by the test.
749pub struct HygieneTester;
750impl Fold for HygieneTester {
751    fn fold_ident(&mut self, ident: Ident) -> Ident {
752        Ident {
753            sym: format!("{}__{}", ident.sym, ident.ctxt.as_u32()).into(),
754            ..ident
755        }
756    }
757
758    fn fold_member_prop(&mut self, p: MemberProp) -> MemberProp {
759        match p {
760            MemberProp::Computed(..) => p.fold_children_with(self),
761            _ => p,
762        }
763    }
764
765    fn fold_prop_name(&mut self, p: PropName) -> PropName {
766        match p {
767            PropName::Computed(..) => p.fold_children_with(self),
768            _ => p,
769        }
770    }
771}
772
773pub struct HygieneVisualizer;
774impl Fold for HygieneVisualizer {
775    fn fold_ident(&mut self, ident: Ident) -> Ident {
776        Ident {
777            sym: format!("{}{:?}", ident.sym, ident.ctxt).into(),
778            ..ident
779        }
780    }
781}
782
783/// Just like babel, walk up the directory tree and find a file named
784/// `options.json`.
785pub fn parse_options<T>(dir: &Path) -> T
786where
787    T: DeserializeOwned,
788{
789    type Map = serde_json::Map<String, serde_json::Value>;
790
791    let mut value = Map::default();
792
793    fn check(dir: &Path) -> Option<Map> {
794        let file = dir.join("options.json");
795        if let Ok(v) = read_to_string(&file) {
796            eprintln!("Using options.json at {}", file.display());
797            eprintln!("----- {} -----\n{}", Color::Green.paint("Options"), v);
798
799            return Some(
800                serde_json::from_str(&v)
801                    .unwrap_or_else(|err| panic!("failed to deserialize options.json: {err}\n{v}")),
802            );
803        }
804
805        None
806    }
807
808    let mut c = Some(dir);
809
810    while let Some(dir) = c {
811        if let Some(new) = check(dir) {
812            for (k, v) in new {
813                if !value.contains_key(&k) {
814                    value.insert(k, v);
815                }
816            }
817        }
818
819        c = dir.parent();
820    }
821
822    serde_json::from_value(serde_json::Value::Object(value.clone()))
823        .unwrap_or_else(|err| panic!("failed to deserialize options.json: {err}\n{value:?}"))
824}
825
826/// Config for [test_fixture]. See [test_fixture] for documentation.
827#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
828pub struct FixtureTestConfig {
829    /// If true, source map will be printed to the `.map` file.
830    ///
831    /// Defaults to false.
832    pub sourcemap: bool,
833
834    /// If true, diagnostics written to [HANDLER] will be printed as a fixture,
835    /// with `.stderr` extension.
836    ///
837    /// If false, test will fail if diagnostics are emitted.
838    ///
839    /// Defaults to false.
840    pub allow_error: bool,
841
842    /// Determines what type of [Program] the source code is parsed as.
843    ///
844    /// - `Some(true)`: parsed as a [Program::Module]
845    /// - `Some(false)`: parsed as a [Program::Script]
846    /// - `None`: parsed as a [Program] (underlying type is auto-detected)
847    pub module: Option<bool>,
848}
849
850/// You can do `UPDATE=1 cargo test` to update fixtures.
851pub fn test_fixture<P>(
852    syntax: Syntax,
853    tr: &dyn Fn(&mut Tester) -> P,
854    input: &Path,
855    output: &Path,
856    config: FixtureTestConfig,
857) where
858    P: Pass,
859{
860    let input = fs::read_to_string(input).unwrap();
861
862    test_fixture_inner(
863        syntax,
864        Box::new(|tester| Box::new(tr(tester))),
865        &input,
866        output,
867        config,
868    );
869}
870
871fn test_fixture_inner<'a>(
872    syntax: Syntax,
873    tr: Box<dyn 'a + FnOnce(&mut Tester) -> Box<dyn 'a + Pass>>,
874    input: &str,
875    output: &Path,
876    config: FixtureTestConfig,
877) {
878    let _logger = testing::init();
879
880    let expected = read_to_string(output);
881    let _is_really_expected = expected.is_ok();
882    let expected = expected.unwrap_or_default();
883
884    let expected_src = Tester::run(|tester| {
885        let expected_program =
886            tester.apply_transform(noop_pass(), "expected.js", syntax, config.module, &expected)?;
887
888        let expected_src = tester.print(&expected_program, &tester.comments.clone());
889
890        println!(
891            "----- {} -----\n{}",
892            Color::Green.paint("Expected"),
893            expected_src
894        );
895
896        Ok(expected_src)
897    });
898
899    let mut src_map = if config.sourcemap {
900        Some(Vec::new())
901    } else {
902        None
903    };
904
905    let mut sourcemap = None;
906
907    let (actual_src, stderr) = Tester::run_captured(|tester| {
908        eprintln!("----- {} -----\n{}", Color::Green.paint("Input"), input);
909
910        let tr = tr(tester);
911
912        eprintln!("----- {} -----", Color::Green.paint("Actual"));
913
914        let actual = tester.apply_transform(tr, "input.js", syntax, config.module, input)?;
915
916        eprintln!("----- {} -----", Color::Green.paint("Comments"));
917        eprintln!("{:?}", tester.comments);
918
919        match ::std::env::var("PRINT_HYGIENE") {
920            Ok(ref s) if s == "1" => {
921                let hygiene_src = tester.print(
922                    &actual.clone().fold_with(&mut HygieneVisualizer),
923                    &tester.comments.clone(),
924                );
925                println!(
926                    "----- {} -----\n{}",
927                    Color::Green.paint("Hygiene"),
928                    hygiene_src
929                );
930            }
931            _ => {}
932        }
933
934        let actual = actual
935            .apply(crate::hygiene::hygiene())
936            .apply(crate::fixer::fixer(Some(&tester.comments)));
937
938        let actual_src = {
939            let module = &actual;
940            let comments: &Rc<SingleThreadedComments> = &tester.comments.clone();
941
942            let mut buf = vec![];
943            {
944                let mut emitter = Emitter {
945                    cfg: Default::default(),
946                    cm: tester.cm.clone(),
947                    wr: Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
948                        tester.cm.clone(),
949                        "\n",
950                        &mut buf,
951                        src_map.as_mut(),
952                    )),
953                    comments: Some(comments),
954                };
955
956                // println!("Emitting: {:?}", module);
957                emitter.emit_program(module).unwrap();
958            }
959
960            if let Some(src_map) = &mut src_map {
961                sourcemap = Some(
962                    tester
963                        .cm
964                        .build_source_map(src_map, None, SourceMapConfigImpl),
965                );
966            }
967
968            String::from_utf8(buf).expect("codegen generated non-utf8 output")
969        };
970
971        Ok(actual_src)
972    });
973
974    if config.allow_error {
975        stderr
976            .compare_to_file(output.with_extension("stderr"))
977            .unwrap();
978    } else if !stderr.is_empty() {
979        panic!("stderr: {stderr}");
980    }
981
982    if let Some(actual_src) = actual_src {
983        eprintln!("{actual_src}");
984
985        if let Some(sourcemap) = &sourcemap {
986            eprintln!("----- ----- ----- ----- -----");
987            eprintln!("SourceMap: {}", visualizer_url(&actual_src, sourcemap));
988        }
989
990        if actual_src != expected_src {
991            NormalizedOutput::from(actual_src)
992                .compare_to_file(output)
993                .unwrap();
994        }
995    }
996
997    if let Some(sourcemap) = sourcemap {
998        let map = {
999            let mut buf = Vec::new();
1000            sourcemap.to_writer(&mut buf).unwrap();
1001            String::from_utf8(buf).unwrap()
1002        };
1003        NormalizedOutput::from(map)
1004            .compare_to_file(output.with_extension("map"))
1005            .unwrap();
1006    }
1007}
1008
1009/// Creates a url for https://evanw.github.io/source-map-visualization/
1010fn visualizer_url(code: &str, map: &swc_sourcemap::SourceMap) -> String {
1011    let map = {
1012        let mut buf = Vec::new();
1013        map.to_writer(&mut buf).unwrap();
1014        String::from_utf8(buf).unwrap()
1015    };
1016
1017    let code_len = format!("{}\0", code.len());
1018    let map_len = format!("{}\0", map.len());
1019    let hash = BASE64_STANDARD.encode(format!("{code_len}{code}{map_len}{map}"));
1020
1021    format!("https://evanw.github.io/source-map-visualization/#{hash}")
1022}
1023
1024struct SourceMapConfigImpl;
1025
1026impl SourceMapGenConfig for SourceMapConfigImpl {
1027    fn file_name_to_source(&self, f: &swc_common::FileName) -> String {
1028        f.to_string()
1029    }
1030
1031    fn inline_sources_content(&self, _: &swc_common::FileName) -> bool {
1032        true
1033    }
1034}