swc_ecma_transforms_testing/
babel_like.rs

1use std::{fs::read_to_string, path::Path};
2
3use ansi_term::Color;
4use serde::Deserialize;
5use serde_json::Value;
6use swc_common::{comments::SingleThreadedComments, sync::Lrc, Mark, SourceMap};
7use swc_ecma_ast::{EsVersion, Pass, Program};
8use swc_ecma_codegen::Emitter;
9use swc_ecma_parser::{parse_file_as_program, Syntax};
10use swc_ecma_transforms_base::{
11    assumptions::Assumptions,
12    fixer::fixer,
13    helpers::{inject_helpers, Helpers, HELPERS},
14    hygiene::hygiene,
15    resolver,
16};
17use testing::NormalizedOutput;
18
19use crate::{exec_with_node_test_runner, parse_options, stdout_of};
20
21pub type PassFactory<'a> =
22    Box<dyn 'a + FnMut(&PassContext, &str, Option<Value>) -> Option<Box<dyn 'static + Pass>>>;
23
24/// These tests use `options.json`.
25///
26///
27/// Note: You should **not** use [resolver] by yourself.
28pub struct BabelLikeFixtureTest<'a> {
29    input: &'a Path,
30
31    /// Default to [`Syntax::default`]
32    syntax: Syntax,
33
34    factories: Vec<Box<dyn 'a + FnOnce() -> PassFactory<'a>>>,
35
36    source_map: bool,
37    allow_error: bool,
38}
39
40impl<'a> BabelLikeFixtureTest<'a> {
41    pub fn new(input: &'a Path) -> Self {
42        Self {
43            input,
44            syntax: Default::default(),
45            factories: Default::default(),
46            source_map: false,
47            allow_error: false,
48        }
49    }
50
51    pub fn syntax(mut self, syntax: Syntax) -> Self {
52        self.syntax = syntax;
53        self
54    }
55
56    pub fn source_map(mut self) -> Self {
57        self.source_map = true;
58        self
59    }
60
61    pub fn allow_error(mut self) -> Self {
62        self.source_map = true;
63        self
64    }
65
66    /// This takes a closure which returns a [PassFactory]. This is because you
67    /// may need to create [Mark], which requires [swc_common::GLOBALS] to be
68    /// configured.
69    pub fn add_factory(mut self, factory: impl 'a + FnOnce() -> PassFactory<'a>) -> Self {
70        self.factories.push(Box::new(factory));
71        self
72    }
73
74    fn run(self, output_path: Option<&Path>, compare_stdout: bool) {
75        let err = testing::run_test(false, |cm, handler| {
76            let mut factories = self.factories.into_iter().map(|f| f()).collect::<Vec<_>>();
77
78            let options = parse_options::<BabelOptions>(self.input.parent().unwrap());
79
80            let comments = SingleThreadedComments::default();
81            let mut builder = PassContext {
82                cm: cm.clone(),
83                assumptions: options.assumptions,
84                unresolved_mark: Mark::new(),
85                top_level_mark: Mark::new(),
86                comments: comments.clone(),
87            };
88
89            let mut pass: Box<dyn Pass> = Box::new(resolver(
90                builder.unresolved_mark,
91                builder.top_level_mark,
92                self.syntax.typescript(),
93            ));
94
95            // Build pass using babel options
96
97            //
98            for plugin in options.plugins {
99                let (name, options) = match plugin {
100                    BabelPluginEntry::NameOnly(name) => (name, None),
101                    BabelPluginEntry::WithConfig(name, options) => (name, Some(options)),
102                };
103
104                let mut done = false;
105                for factory in &mut factories {
106                    if let Some(built) = factory(&builder, &name, options.clone()) {
107                        pass = Box::new((pass, built));
108                        done = true;
109                        break;
110                    }
111                }
112
113                if !done {
114                    panic!("Unknown plugin: {name}");
115                }
116            }
117
118            pass = Box::new((pass, hygiene(), fixer(Some(&comments))));
119
120            // Run pass
121
122            let src = read_to_string(self.input).expect("failed to read file");
123            let src = if output_path.is_none() && !compare_stdout {
124                format!(
125                    "it('should work', async function () {{
126                    {src}
127                }})",
128                )
129            } else {
130                src
131            };
132            let fm = cm.new_source_file(
133                swc_common::FileName::Real(self.input.to_path_buf()).into(),
134                src,
135            );
136
137            let mut errors = Vec::new();
138            let input_program = parse_file_as_program(
139                &fm,
140                self.syntax,
141                EsVersion::latest(),
142                Some(&comments),
143                &mut errors,
144            );
145
146            let errored = !errors.is_empty();
147
148            for e in errors {
149                e.into_diagnostic(handler).emit();
150            }
151
152            let input_program = match input_program {
153                Ok(v) => v,
154                Err(err) => {
155                    err.into_diagnostic(handler).emit();
156                    return Err(());
157                }
158            };
159
160            if errored {
161                return Err(());
162            }
163
164            let helpers = Helpers::new(output_path.is_some());
165            let (code_without_helper, output_program) = HELPERS.set(&helpers, || {
166                let mut p = input_program.apply(pass);
167
168                let code_without_helper = builder.print(&p);
169
170                if output_path.is_none() {
171                    p.mutate(inject_helpers(builder.unresolved_mark))
172                }
173
174                (code_without_helper, p)
175            });
176
177            // Print output
178            let code = builder.print(&output_program);
179
180            println!(
181                "\t>>>>> {} <<<<<\n{}\n\t>>>>> {} <<<<<\n{}",
182                Color::Green.paint("Orig"),
183                fm.src,
184                Color::Green.paint("Code"),
185                code_without_helper
186            );
187
188            if let Some(output_path) = output_path {
189                // Fixture test
190
191                if !self.allow_error && handler.has_errors() {
192                    return Err(());
193                }
194
195                NormalizedOutput::from(code)
196                    .compare_to_file(output_path)
197                    .unwrap();
198            } else if compare_stdout {
199                // Execution test, but compare stdout
200
201                let actual_stdout: String =
202                    stdout_of(&code).expect("failed to execute transfomred code");
203                let expected_stdout =
204                    stdout_of(&fm.src).expect("failed to execute transfomred code");
205
206                testing::assert_eq!(actual_stdout, expected_stdout);
207            } else {
208                // Execution test
209
210                exec_with_node_test_runner(&format!("// {}\n{code}", self.input.display()))
211                    .expect("failed to execute transfomred code");
212            }
213
214            Ok(())
215        });
216
217        if self.allow_error {
218            match err {
219                Ok(_) => {}
220                Err(err) => {
221                    err.compare_to_file(self.input.with_extension("stderr"))
222                        .unwrap();
223                }
224            }
225        }
226    }
227
228    /// Execute using node.js and mocha
229    pub fn exec_with_test_runner(self) {
230        self.run(None, false)
231    }
232
233    /// Execute using node.js
234    pub fn compare_stdout(self) {
235        self.run(None, true)
236    }
237
238    /// Run a fixture test
239    pub fn fixture(self, output: &Path) {
240        self.run(Some(output), false)
241    }
242}
243
244#[derive(Debug, Deserialize)]
245struct BabelOptions {
246    #[serde(default)]
247    assumptions: Assumptions,
248
249    #[serde(default)]
250    plugins: Vec<BabelPluginEntry>,
251}
252
253#[derive(Debug, Deserialize)]
254#[serde(deny_unknown_fields, rename_all = "camelCase", untagged)]
255enum BabelPluginEntry {
256    NameOnly(String),
257    WithConfig(String, Value),
258}
259
260#[derive(Clone)]
261pub struct PassContext {
262    pub cm: Lrc<SourceMap>,
263
264    pub assumptions: Assumptions,
265    pub unresolved_mark: Mark,
266    pub top_level_mark: Mark,
267
268    /// [SingleThreadedComments] is cheap to clone.
269    pub comments: SingleThreadedComments,
270}
271
272impl PassContext {
273    fn print(&mut self, program: &Program) -> String {
274        let mut buf = Vec::new();
275        {
276            let mut emitter = Emitter {
277                cfg: Default::default(),
278                cm: self.cm.clone(),
279                wr: Box::new(swc_ecma_codegen::text_writer::JsWriter::new(
280                    self.cm.clone(),
281                    "\n",
282                    &mut buf,
283                    None,
284                )),
285                comments: Some(&self.comments),
286            };
287
288            emitter.emit_program(program).unwrap();
289        }
290
291        let s = String::from_utf8_lossy(&buf);
292        s.to_string()
293    }
294}