swc_ecma_transforms_testing/
babel_like.rs1use 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
24pub struct BabelLikeFixtureTest<'a> {
29 input: &'a Path,
30
31 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 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 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 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 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 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 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 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 pub fn exec_with_test_runner(self) {
230 self.run(None, false)
231 }
232
233 pub fn compare_stdout(self) {
235 self.run(None, true)
236 }
237
238 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 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}