From e9606147fc560cac2ffa75b917a08413b7c94908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Donny/=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Mon, 30 Oct 2023 07:24:49 +0900 Subject: [PATCH] feat(es/testing): Support babel-like fixture testing officially (#8190) --- .../tests/es2015_arrow.rs | 2 +- .../allow-continuous-assignment/input.js | 1 + .../tests/system_js.rs | 18 -- .../output.mjs | 7 +- .../output.mjs | 3 +- .../src/babel_like.rs | 293 ++++++++++++++++++ crates/swc_ecma_transforms_testing/src/lib.rs | 47 ++- 7 files changed, 333 insertions(+), 38 deletions(-) create mode 100644 crates/swc_ecma_transforms_module/tests/fixture/systemjs/allow-continuous-assignment/input.js create mode 100644 crates/swc_ecma_transforms_testing/src/babel_like.rs diff --git a/crates/swc_ecma_transforms_compat/tests/es2015_arrow.rs b/crates/swc_ecma_transforms_compat/tests/es2015_arrow.rs index edf525180c39..9f8738033a89 100644 --- a/crates/swc_ecma_transforms_compat/tests/es2015_arrow.rs +++ b/crates/swc_ecma_transforms_compat/tests/es2015_arrow.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use swc_common::{chain, Mark}; use swc_ecma_transforms_base::resolver; use swc_ecma_transforms_compat::es2015::arrow; -use swc_ecma_transforms_testing::{compare_stdout, test, test_fixture}; +use swc_ecma_transforms_testing::{compare_stdout, test_fixture}; use swc_ecma_visit::Fold; fn tr() -> impl Fold { diff --git a/crates/swc_ecma_transforms_module/tests/fixture/systemjs/allow-continuous-assignment/input.js b/crates/swc_ecma_transforms_module/tests/fixture/systemjs/allow-continuous-assignment/input.js new file mode 100644 index 000000000000..04b470e001e3 --- /dev/null +++ b/crates/swc_ecma_transforms_module/tests/fixture/systemjs/allow-continuous-assignment/input.js @@ -0,0 +1 @@ +var e = {}; e.a = e.b = e.c = e.d = e.e = e.f = e.g = e.h = e.i = e.j = e.k = e.l = e.m = e.n = e.o = e.p = e.q = e.r = e.s = e.t = e.u = e.v = e.w = e.x = e.y = e.z = e.A = e.B = e.C = e.D = e.E = e.F = e.G = e.H = e.I = e.J = e.K = e.L = e.M = e.N = e.O = e.P = e.Q = e.R = e.S = void 0; \ No newline at end of file diff --git a/crates/swc_ecma_transforms_module/tests/system_js.rs b/crates/swc_ecma_transforms_module/tests/system_js.rs index 998178ab3cdf..65033f690316 100644 --- a/crates/swc_ecma_transforms_module/tests/system_js.rs +++ b/crates/swc_ecma_transforms_module/tests/system_js.rs @@ -22,24 +22,6 @@ fn tr(_tester: &mut Tester<'_>, config: Config) -> impl Fold { ) } -test!( - syntax(), - |tester| tr(tester, Default::default()), - allow_continuous_assignment, - r#"var e = {}; e.a = e.b = e.c = e.d = e.e = e.f = e.g = e.h = e.i = e.j = e.k = e.l = e.m = e.n = e.o = e.p = e.q = e.r = e.s = e.t = e.u = e.v = e.w = e.x = e.y = e.z = e.A = e.B = e.C = e.D = e.E = e.F = e.G = e.H = e.I = e.J = e.K = e.L = e.M = e.N = e.O = e.P = e.Q = e.R = e.S = void 0;"#, - r#"System.register([], function (_export, _context) { - "use strict"; - var e; - return { - setters: [], - execute: function () { - e = {}; - e.a = e.b = e.c = e.d = e.e = e.f = e.g = e.h = e.i = e.j = e.k = e.l = e.m = e.n = e.o = e.p = e.q = e.r = e.s = e.t = e.u = e.v = e.w = e.x = e.y = e.z = e.A = e.B = e.C = e.D = e.E = e.F = e.G = e.H = e.I = e.J = e.K = e.L = e.M = e.N = e.O = e.P = e.Q = e.R = e.S = void 0; - } - }; - });"# -); - test!( syntax(), |tester| tr( diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-spread-children/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-spread-children/output.mjs index b8d3602b8bbd..8502e2354a34 100644 --- a/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-spread-children/output.mjs +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-spread-children/output.mjs @@ -1 +1,6 @@ -/*#__PURE__*/ React.createElement("div", null, ...children); +/*#__PURE__*/ import { jsx as _jsx } from "react/jsx-runtime"; +_jsx("div", { + children: [ + ...children + ] +}); diff --git a/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-xml-namespacing/output.mjs b/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-xml-namespacing/output.mjs index 555ef8ee0d1d..fd9baf502234 100644 --- a/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-xml-namespacing/output.mjs +++ b/crates/swc_ecma_transforms_react/tests/jsx/fixture/react-automatic/should-disallow-xml-namespacing/output.mjs @@ -1 +1,2 @@ -/*#__PURE__*/ React.createElement("Namespace:Component", null); +/*#__PURE__*/ import { jsx as _jsx } from "react/jsx-runtime"; +_jsx("Namespace:Component", {}); diff --git a/crates/swc_ecma_transforms_testing/src/babel_like.rs b/crates/swc_ecma_transforms_testing/src/babel_like.rs new file mode 100644 index 000000000000..56851e935bfb --- /dev/null +++ b/crates/swc_ecma_transforms_testing/src/babel_like.rs @@ -0,0 +1,293 @@ +use std::{fs::read_to_string, path::Path}; + +use ansi_term::Color; +use serde::Deserialize; +use serde_json::Value; +use swc_common::{chain, comments::SingleThreadedComments, sync::Lrc, Mark, SourceMap}; +use swc_ecma_ast::{EsVersion, Program}; +use swc_ecma_codegen::Emitter; +use swc_ecma_parser::{parse_file_as_program, Syntax}; +use swc_ecma_transforms_base::{ + assumptions::Assumptions, + fixer::fixer, + helpers::{inject_helpers, Helpers, HELPERS}, + hygiene::hygiene, + resolver, +}; +use swc_ecma_visit::{Fold, FoldWith, VisitMutWith}; +use testing::NormalizedOutput; + +use crate::{exec_with_node_test_runner, parse_options, stdout_of}; + +pub type PassFactory<'a> = + Box) -> Option>>; + +/// These tests use `options.json`. +/// +/// +/// Note: You should **not** use [resolver] by yourself. + +pub struct BabelLikeFixtureTest<'a> { + input: &'a Path, + + /// Default to [`Syntax::default`] + syntax: Syntax, + + factories: Vec PassFactory<'a>>>, + + source_map: bool, + allow_error: bool, +} + +impl<'a> BabelLikeFixtureTest<'a> { + pub fn new(input: &'a Path) -> Self { + Self { + input, + syntax: Default::default(), + factories: Default::default(), + source_map: false, + allow_error: false, + } + } + + pub fn syntax(mut self, syntax: Syntax) -> Self { + self.syntax = syntax; + self + } + + pub fn source_map(mut self) -> Self { + self.source_map = true; + self + } + + pub fn allow_error(mut self) -> Self { + self.source_map = true; + self + } + + /// This takes a closure which returns a [PassFactory]. This is because you + /// may need to create [Mark], which requires [swc_common::GLOBALS] to be + /// configured. + pub fn add_factory(mut self, factory: impl 'a + FnOnce() -> PassFactory<'a>) -> Self { + self.factories.push(Box::new(factory)); + self + } + + fn run(self, output_path: Option<&Path>, compare_stdout: bool) { + let err = testing::run_test(false, |cm, handler| { + let mut factories = self.factories.into_iter().map(|f| f()).collect::>(); + + let options = parse_options::(self.input.parent().unwrap()); + + let comments = SingleThreadedComments::default(); + let mut builder = PassContext { + cm: cm.clone(), + assumptions: options.assumptions, + unresolved_mark: Mark::new(), + top_level_mark: Mark::new(), + comments: comments.clone(), + }; + + let mut pass: Box = Box::new(resolver( + builder.unresolved_mark, + builder.top_level_mark, + self.syntax.typescript(), + )); + + // Build pass using babel options + + // + for plugin in options.plugins { + let (name, options) = match plugin { + BabelPluginEntry::NameOnly(name) => (name, None), + BabelPluginEntry::WithConfig(name, options) => (name, Some(options)), + }; + + let mut done = false; + for factory in &mut factories { + if let Some(built) = factory(&builder, &name, options.clone()) { + pass = Box::new(chain!(pass, built)); + done = true; + break; + } + } + + if !done { + panic!("Unknown plugin: {}", name); + } + } + + pass = Box::new(chain!(pass, hygiene(), fixer(Some(&comments)))); + + // Run pass + + let src = read_to_string(self.input).expect("failed to read file"); + let src = if output_path.is_none() && !compare_stdout { + format!( + "it('should work', async function () {{ + {src} + }})", + ) + } else { + src + }; + let fm = cm.new_source_file(swc_common::FileName::Real(self.input.to_path_buf()), src); + + let mut errors = vec![]; + let input_program = parse_file_as_program( + &fm, + self.syntax, + EsVersion::latest(), + Some(&comments), + &mut errors, + ); + + let errored = !errors.is_empty(); + + for e in errors { + e.into_diagnostic(handler).emit(); + } + + let input_program = match input_program { + Ok(v) => v, + Err(err) => { + err.into_diagnostic(handler).emit(); + return Err(()); + } + }; + + if errored { + return Err(()); + } + + let helpers = Helpers::new(output_path.is_some()); + let (code_without_helper, output_program) = HELPERS.set(&helpers, || { + let mut p = input_program.fold_with(&mut *pass); + + let code_without_helper = builder.print(&p); + + if output_path.is_none() { + p.visit_mut_with(&mut inject_helpers(builder.unresolved_mark)) + } + + (code_without_helper, p) + }); + + // Print output + let code = builder.print(&output_program); + + println!( + "\t>>>>> {} <<<<<\n{}\n\t>>>>> {} <<<<<\n{}", + Color::Green.paint("Orig"), + fm.src, + Color::Green.paint("Code"), + code_without_helper + ); + + if let Some(output_path) = output_path { + // Fixture test + + if !self.allow_error && handler.has_errors() { + return Err(()); + } + + NormalizedOutput::from(code) + .compare_to_file(output_path) + .unwrap(); + } else if compare_stdout { + // Execution test, but compare stdout + + let actual_stdout: String = + stdout_of(&code).expect("failed to execute transfomred code"); + let expected_stdout = + stdout_of(&fm.src).expect("failed to execute transfomred code"); + + testing::assert_eq!(actual_stdout, expected_stdout); + } else { + // Execution test + + exec_with_node_test_runner(&format!("// {}\n{code}", self.input.display())) + .expect("failed to execute transfomred code"); + } + + Ok(()) + }); + + if self.allow_error { + match err { + Ok(_) => {} + Err(err) => { + err.compare_to_file(self.input.with_extension("stderr")) + .unwrap(); + } + } + } + } + + /// Execute using node.js and mocha + pub fn exec_with_test_runner(self) { + self.run(None, false) + } + + /// Execute using node.js + pub fn compare_stdout(self) { + self.run(None, true) + } + + /// Run a fixture test + pub fn fixture(self, output: &Path) { + self.run(Some(output), false) + } +} + +#[derive(Debug, Deserialize)] +struct BabelOptions { + #[serde(default)] + assumptions: Assumptions, + + #[serde(default)] + plugins: Vec, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields, rename_all = "camelCase", untagged)] +enum BabelPluginEntry { + NameOnly(String), + WithConfig(String, Value), +} + +#[derive(Clone)] +pub struct PassContext { + pub cm: Lrc, + + pub assumptions: Assumptions, + pub unresolved_mark: Mark, + pub top_level_mark: Mark, + + /// [SingleThreadedComments] is cheap to clone. + pub comments: SingleThreadedComments, +} + +impl PassContext { + fn print(&mut self, program: &Program) -> String { + let mut buf = vec![]; + { + let mut emitter = Emitter { + cfg: Default::default(), + cm: self.cm.clone(), + wr: Box::new(swc_ecma_codegen::text_writer::JsWriter::new( + self.cm.clone(), + "\n", + &mut buf, + None, + )), + comments: Some(&self.comments), + }; + + emitter.emit_program(program).unwrap(); + } + + let s = String::from_utf8_lossy(&buf); + s.to_string() + } +} diff --git a/crates/swc_ecma_transforms_testing/src/lib.rs b/crates/swc_ecma_transforms_testing/src/lib.rs index c97f2db3865e..fb8098d275ea 100644 --- a/crates/swc_ecma_transforms_testing/src/lib.rs +++ b/crates/swc_ecma_transforms_testing/src/lib.rs @@ -39,7 +39,9 @@ use swc_ecma_transforms_base::{ use swc_ecma_utils::{quote_ident, quote_str, ExprFactory}; use swc_ecma_visit::{as_folder, noop_visit_mut_type, Fold, FoldWith, VisitMut, VisitMutWith}; use tempfile::tempdir_in; -use testing::{assert_eq, find_executable, NormalizedOutput}; +use testing::{assert_eq, find_executable, NormalizedOutput, CARGO_TARGET_DIR}; + +pub mod babel_like; pub struct Tester<'a> { pub cm: Lrc, @@ -421,7 +423,7 @@ where } /// Execute `jest` after transpiling `input` using `tr`. -pub fn exec_tr(test_name: &str, syntax: Syntax, tr: F, input: &str) +pub fn exec_tr(_test_name: &str, syntax: Syntax, tr: F, input: &str) where F: FnOnce(&mut Tester<'_>) -> P, P: Fold, @@ -468,7 +470,7 @@ where src_without_helpers ); - exec_with_node_test_runner(test_name, &src) + exec_with_node_test_runner(&src).map(|_| {}) }) } @@ -480,11 +482,8 @@ fn calc_hash(s: &str) -> String { hex::encode(sum) } -fn exec_with_node_test_runner(test_name: &str, src: &str) -> Result<(), ()> { - let root = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("target") - .join("testing") - .join(test_name); +fn exec_with_node_test_runner(src: &str) -> Result<(), ()> { + let root = CARGO_TARGET_DIR.join("swc-es-exec-testing"); create_dir_all(&root).expect("failed to create parent directory for temp directory"); @@ -503,7 +502,7 @@ fn exec_with_node_test_runner(test_name: &str, src: &str) -> Result<(), ()> { let tmp_dir = tempdir_in(&root).expect("failed to create a temp directory"); create_dir_all(&tmp_dir).unwrap(); - let path = tmp_dir.path().join(format!("{}.test.js", test_name)); + let path = tmp_dir.path().join(format!("{}.test.js", hash)); let mut tmp = OpenOptions::new() .create(true) @@ -644,26 +643,40 @@ pub fn parse_options(dir: &Path) -> T where T: DeserializeOwned, { - let mut s = String::from("{}"); + type Map = serde_json::Map; + + let mut value = Map::default(); - fn check(dir: &Path) -> Option { + fn check(dir: &Path) -> Option { let file = dir.join("options.json"); if let Ok(v) = read_to_string(&file) { eprintln!("Using options.json at {}", file.display()); eprintln!("----- {} -----\n{}", Color::Green.paint("Options"), v); - return Some(v); + return Some(serde_json::from_str(&v).unwrap_or_else(|err| { + panic!("failed to deserialize options.json: {}\n{}", err, v) + })); } - dir.parent().and_then(check) + None } - if let Some(content) = check(dir) { - s = content; + let mut c = Some(dir); + + while let Some(dir) = c { + if let Some(new) = check(dir) { + for (k, v) in new { + if !value.contains_key(&k) { + value.insert(k, v); + } + } + } + + c = dir.parent(); } - serde_json::from_str(&s) - .unwrap_or_else(|err| panic!("failed to deserialize options.json: {}\n{}", err, s)) + serde_json::from_value(serde_json::Value::Object(value.clone())) + .unwrap_or_else(|err| panic!("failed to deserialize options.json: {}\n{:?}", err, value)) } /// Config for [test_fixture]. See [test_fixture] for documentation.