feat(transformer_conformance): read plugins options from babel options.json (#1006)

This PR correctly handles babel `options.json` such as
https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-nullish-coalescing-operator/test/fixtures/assumption-noDocumentAll/options.json
This commit is contained in:
Boshen 2023-10-17 14:52:51 +08:00 committed by GitHub
parent 077585addc
commit 1b3b100475
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 497 additions and 346 deletions

7
Cargo.lock generated
View file

@ -1793,7 +1793,10 @@ dependencies = [
name = "oxc_tasks_common"
version = "0.0.0"
dependencies = [
"oxc_syntax",
"project-root",
"serde",
"serde_json",
"ureq",
"url",
]
@ -1809,9 +1812,12 @@ dependencies = [
"oxc_parser",
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"oxc_tasks_common",
"oxc_transformer",
"pico-args",
"serde",
"serde_json",
"walkdir",
]
@ -1826,6 +1832,7 @@ dependencies = [
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"serde",
]
[[package]]

View file

@ -15,10 +15,6 @@ categories.workspace = true
[lib]
doctest = false
[features]
default = []
serde = ["dep:serde"]
[dependencies]
oxc_index = { workspace = true }
oxc_span = { workspace = true }
@ -29,3 +25,7 @@ bitflags = { workspace = true }
rustc-hash = { workspace = true }
indexmap = { workspace = true }
dashmap = { workspace = true }
[features]
default = []
serde = ["dep:serde"]

View file

@ -0,0 +1,15 @@
#[cfg(feature = "serde")]
use serde::Deserialize;
/// Compiler assumptions
///
/// See <https://babeljs.io/docs/assumptions>
#[derive(Debug, Default, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct CompilerAssumptions {
/// When using operators that check for null or undefined, assume that they are never used with the special value document.all.
/// See <https://babeljs.io/docs/assumptions#nodocumentall>.
#[cfg_attr(feature = "serde", serde(default))]
pub no_document_all: bool,
}

View file

@ -1,5 +1,6 @@
//! Common code for JavaScript Syntax
pub mod assumptions;
pub mod identifier;
pub mod module_record;
pub mod operator;

View file

@ -22,6 +22,8 @@ oxc_allocator = { workspace = true }
oxc_syntax = { workspace = true }
oxc_semantic = { workspace = true }
serde = { workspace = true }
[dev-dependencies]
oxc_parser = { workspace = true }
oxc_codegen = { workspace = true }

View file

@ -5,9 +5,7 @@ use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_transformer::{
Assumptions, TransformOptions, TransformReactOptions, TransformTarget, Transformer,
};
use oxc_transformer::{TransformOptions, TransformTarget, Transformer};
// Instruction:
// create a `test.js`,
@ -40,11 +38,8 @@ fn main() {
let symbols = Rc::new(RefCell::new(symbols));
let program = allocator.alloc(ret.program);
let transform_options = TransformOptions {
target: TransformTarget::ES2015,
react: Some(TransformReactOptions::default()),
assumptions: Assumptions::default(),
};
let transform_options =
TransformOptions { target: TransformTarget::ES2015, ..TransformOptions::default() };
Transformer::new(&allocator, source_type, &symbols, transform_options).build(program);
let printed = Codegen::<false>::new(source_text.len(), codegen_options).build(program);
println!("Transformed:\n");

View file

@ -1,7 +1,9 @@
use std::rc::Rc;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::GetSpan;
use std::rc::Rc;
use crate::options::{TransformOptions, TransformTarget};
/// ES2015: Shorthand Properties
///
@ -13,8 +15,9 @@ pub struct ShorthandProperties<'a> {
}
impl<'a> ShorthandProperties<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
Self { ast }
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(options.target < TransformTarget::ES2015 || options.shorthand_properties)
.then(|| Self { ast })
}
pub fn transform_object_property<'b>(&mut self, obj_prop: &'b mut ObjectProperty<'a>) {

View file

@ -6,7 +6,10 @@ use oxc_semantic::SymbolTable;
use oxc_span::{Atom, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator};
use crate::utils::CreateVars;
use crate::{
options::{TransformOptions, TransformTarget},
utils::CreateVars,
};
/// ES2016: Exponentiation Operator
///
@ -36,9 +39,15 @@ impl<'a> CreateVars<'a> for ExponentiationOperator<'a> {
}
impl<'a> ExponentiationOperator<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, symbols: Rc<RefCell<SymbolTable>>) -> Self {
let vars = ast.new_vec();
Self { ast, symbols, vars }
pub fn new(
ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>,
options: &TransformOptions,
) -> Option<Self> {
(options.target < TransformTarget::ES2016 || options.exponentiation_operator).then(|| {
let vars = ast.new_vec();
Self { ast, symbols, vars }
})
}
pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {

View file

@ -1,7 +1,9 @@
use std::rc::Rc;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::Span;
use std::rc::Rc;
use crate::options::{TransformOptions, TransformTarget};
/// ES2019: Optional Catch Binding
///
@ -13,8 +15,9 @@ pub struct OptionalCatchBinding<'a> {
}
impl<'a> OptionalCatchBinding<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
Self { ast }
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(options.target < TransformTarget::ES2019 || options.optional_catch_binding)
.then(|| Self { ast })
}
pub fn transform_catch_clause<'b>(&mut self, clause: &'b mut CatchClause<'a>) {

View file

@ -1,3 +1,5 @@
mod nullish_coalescing_operator;
pub use nullish_coalescing_operator::NullishCoalescingOperator;
pub use nullish_coalescing_operator::{
NullishCoalescingOperator, NullishCoalescingOperatorOptions,
};

View file

@ -1,3 +1,4 @@
use serde::Deserialize;
use std::{cell::RefCell, rc::Rc};
use oxc_allocator::Vec;
@ -6,7 +7,15 @@ use oxc_semantic::SymbolTable;
use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator};
use crate::{options::Assumptions, utils::CreateVars};
use crate::{utils::CreateVars, TransformOptions, TransformTarget};
#[derive(Debug, Default, Clone, Copy, Deserialize)]
pub struct NullishCoalescingOperatorOptions {
/// When true, this transform will pretend `document.all` does not exist,
/// and perform loose equality checks with null instead of strict equality checks against both null and undefined.
#[serde(default)]
loose: bool,
}
/// ES2020: Nullish Coalescing Operator
///
@ -14,9 +23,10 @@ use crate::{options::Assumptions, utils::CreateVars};
/// * <https://babeljs.io/docs/babel-plugin-transform-nullish-coalescing-operator>
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-nullish-coalescing-operator>
pub struct NullishCoalescingOperator<'a> {
no_document_all: bool,
ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions,
vars: Vec<'a, VariableDeclarator<'a>>,
}
@ -34,10 +44,15 @@ impl<'a> NullishCoalescingOperator<'a> {
pub fn new(
ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions,
) -> Self {
let vars = ast.new_vec();
Self { ast, symbols, assumptions, vars }
options: &TransformOptions,
) -> Option<Self> {
(options.target < TransformTarget::ES2020 || options.nullish_coalescing_operator.is_some())
.then(|| {
let no_document_all = options.assumptions.no_document_all
|| options.nullish_coalescing_operator.is_some_and(|o| o.loose);
let vars = ast.new_vec();
Self { no_document_all, ast, symbols, vars }
})
}
pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
@ -67,7 +82,7 @@ impl<'a> NullishCoalescingOperator<'a> {
self.ast.assignment_expression(span, AssignmentOperator::Assign, left, right);
};
let test = if self.assumptions.no_document_all {
let test = if self.no_document_all {
let null = self.ast.literal_null_expression(NullLiteral::new(span));
self.ast.binary_expression(span, assignment, BinaryOperator::Inequality, null)
} else {

View file

@ -1,8 +1,10 @@
use std::rc::Rc;
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, LogicalOperator};
use std::rc::Rc;
use crate::options::{TransformOptions, TransformTarget};
/// ES2021: Logical Assignment Operators
///
@ -14,8 +16,9 @@ pub struct LogicalAssignmentOperators<'a> {
}
impl<'a> LogicalAssignmentOperators<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
Self { ast }
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(options.target < TransformTarget::ES2021 || options.logical_assignment_operators)
.then(|| Self { ast })
}
pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) {

View file

@ -1,7 +1,9 @@
use std::{collections::HashSet, rc::Rc};
use oxc_ast::{ast::*, AstBuilder};
use oxc_span::{Atom, Span};
use std::{collections::HashSet, rc::Rc};
use crate::options::{TransformOptions, TransformTarget};
/// ES2022: Class Static Block
///
@ -13,8 +15,9 @@ pub struct ClassStaticBlock<'a> {
}
impl<'a> ClassStaticBlock<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
Self { ast }
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
(options.target < TransformTarget::ES2022 || options.class_static_block)
.then(|| Self { ast })
}
pub fn transform_class_body<'b>(&mut self, class_body: &'b mut ClassBody<'a>) {

View file

@ -30,12 +30,15 @@ use oxc_span::SourceType;
use crate::{
es2015::ShorthandProperties, es2016::ExponentiationOperator, es2019::OptionalCatchBinding,
es2020::NullishCoalescingOperator, es2021::LogicalAssignmentOperators, react_jsx::ReactJsx,
regexp::RegexpFlags, typescript::TypeScript, utils::CreateVars,
es2020::NullishCoalescingOperator, es2021::LogicalAssignmentOperators,
es2022::ClassStaticBlock, react_jsx::ReactJsx, regexp::RegexpFlags, typescript::TypeScript,
utils::CreateVars,
};
pub use crate::options::{
Assumptions, TransformOptions, TransformReactOptions, TransformReactRuntime, TransformTarget,
pub use crate::{
es2020::NullishCoalescingOperatorOptions,
options::{TransformOptions, TransformTarget},
react_jsx::{ReactJsxOptions, ReactJsxRuntime},
};
#[derive(Default)]
@ -46,7 +49,7 @@ pub struct Transformer<'a> {
react_jsx: Option<ReactJsx<'a>>,
regexp_flags: Option<RegexpFlags<'a>>,
// es2022
es2022_class_static_block: Option<es2022::ClassStaticBlock<'a>>,
es2022_class_static_block: Option<ClassStaticBlock<'a>>,
// es2021
es2021_logical_assignment_operators: Option<LogicalAssignmentOperators<'a>>,
// es2020
@ -70,14 +73,14 @@ impl<'a> Transformer<'a> {
let ast = Rc::new(AstBuilder::new(allocator));
Self {
typescript: source_type.is_typescript().then(|| TypeScript::new(Rc::clone(&ast))),
react_jsx: options.react.map(|options| ReactJsx::new(Rc::clone(&ast), options)),
regexp_flags: RegexpFlags::new(Rc::clone(&ast), options.target),
es2022_class_static_block: (options.target < TransformTarget::ES2022).then(|| es2022::ClassStaticBlock::new(Rc::clone(&ast))),
es2021_logical_assignment_operators: (options.target < TransformTarget::ES2021).then(|| LogicalAssignmentOperators::new(Rc::clone(&ast))),
es2020_nullish_coalescing_operators: (options.target < TransformTarget::ES2020).then(|| NullishCoalescingOperator::new(Rc::clone(&ast), Rc::clone(symbols), options.assumptions)),
es2019_optional_catch_binding: (options.target < TransformTarget::ES2019).then(|| OptionalCatchBinding::new(Rc::clone(&ast))),
es2016_exponentiation_operator: (options.target < TransformTarget::ES2016).then(|| ExponentiationOperator::new(Rc::clone(&ast), Rc::clone(symbols))),
es2015_shorthand_properties: (options.target < TransformTarget::ES2015).then(|| ShorthandProperties::new(Rc::clone(&ast))),
react_jsx: options.react_jsx.map(|options| ReactJsx::new(Rc::clone(&ast), options)),
regexp_flags: RegexpFlags::new(Rc::clone(&ast), &options),
es2022_class_static_block: es2022::ClassStaticBlock::new(Rc::clone(&ast), &options),
es2021_logical_assignment_operators: LogicalAssignmentOperators::new(Rc::clone(&ast), &options),
es2020_nullish_coalescing_operators: NullishCoalescingOperator::new(Rc::clone(&ast), Rc::clone(symbols), &options),
es2019_optional_catch_binding: OptionalCatchBinding::new(Rc::clone(&ast), &options),
es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), Rc::clone(symbols), &options),
es2015_shorthand_properties: ShorthandProperties::new(Rc::clone(&ast), &options),
}
}

View file

@ -1,8 +1,27 @@
use oxc_syntax::assumptions::CompilerAssumptions;
use crate::{es2020::NullishCoalescingOperatorOptions, react_jsx::ReactJsxOptions};
#[derive(Debug, Default, Clone, Copy)]
pub struct TransformOptions {
pub target: TransformTarget,
pub react: Option<TransformReactOptions>,
pub assumptions: Assumptions,
pub assumptions: CompilerAssumptions,
pub react_jsx: Option<ReactJsxOptions>,
// es2022
pub class_static_block: bool,
// es2021
pub logical_assignment_operators: bool,
// es2020
pub nullish_coalescing_operator: Option<NullishCoalescingOperatorOptions>,
// es2019
pub optional_catch_binding: bool,
// es2016
pub exponentiation_operator: bool,
// es2015
pub shorthand_properties: bool,
pub sticky_regex: bool,
}
/// See <https://www.typescriptlang.org/tsconfig#target>
@ -20,25 +39,3 @@ pub enum TransformTarget {
#[default]
ESNext,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct TransformReactOptions {
_runtime: TransformReactRuntime,
}
#[derive(Debug, Default, Clone, Copy)]
pub enum TransformReactRuntime {
#[default]
Classic,
Automatic,
}
/// Compiler assumptions
///
/// See <https://babeljs.io/docs/assumptions>
#[derive(Debug, Default, Clone, Copy)]
pub struct Assumptions {
/// When using operators that check for null or undefined, assume that they are never used with the special value document.all.
/// See <https://babeljs.io/docs/assumptions#nodocumentall>.
pub no_document_all: bool,
}

View file

@ -2,7 +2,18 @@ use std::rc::Rc;
use oxc_ast::AstBuilder;
use crate::TransformReactOptions;
#[derive(Debug, Default, Clone, Copy)]
pub struct ReactJsxOptions {
_runtime: ReactJsxRuntime,
}
#[derive(Debug, Default, Clone, Copy)]
pub enum ReactJsxRuntime {
#[default]
Classic,
#[allow(unused)]
Automatic,
}
/// Transform React JSX
///
@ -11,11 +22,11 @@ use crate::TransformReactOptions;
/// * <https://github.com/babel/babel/tree/main/packages/babel-helper-builder-react-jsx>
pub struct ReactJsx<'a> {
_ast: Rc<AstBuilder<'a>>,
_options: TransformReactOptions,
_options: ReactJsxOptions,
}
impl<'a> ReactJsx<'a> {
pub fn new(_ast: Rc<AstBuilder<'a>>, _options: TransformReactOptions) -> Self {
pub fn new(_ast: Rc<AstBuilder<'a>>, _options: ReactJsxOptions) -> Self {
Self { _ast, _options }
}
}

View file

@ -3,7 +3,7 @@ use oxc_span::{Atom, Span};
use std::rc::Rc;
use crate::TransformTarget;
use crate::{TransformOptions, TransformTarget};
/// Transforms unsupported regex flags into Regex constructors.
///
@ -20,11 +20,36 @@ pub struct RegexpFlags<'a> {
}
impl<'a> RegexpFlags<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, transform_target: TransformTarget) -> Option<Self> {
let transform_flags = Self::from_transform_target(transform_target);
pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
let transform_flags = Self::from_transform_target(options);
(!transform_flags.is_empty()).then(|| Self { ast, transform_flags })
}
fn from_transform_target(options: &TransformOptions) -> RegExpFlags {
let target = options.target;
let mut flag = RegExpFlags::empty();
if target < TransformTarget::ES2015 || options.sticky_regex {
flag |= RegExpFlags::Y;
}
if target < TransformTarget::ES2015 {
flag |= RegExpFlags::U;
}
if target < TransformTarget::ES2018 {
flag |= RegExpFlags::S;
}
if target < TransformTarget::ES2022 {
flag |= RegExpFlags::D;
}
if target < TransformTarget::ES2024 {
flag |= RegExpFlags::V;
}
if target < TransformTarget::ESNext {
flag |= RegExpFlags::I;
flag |= RegExpFlags::M;
}
flag
}
// `/regex/flags` -> `new RegExp('regex', 'flags')`
pub fn transform_expression(&self, expr: &mut Expression<'a>) {
let Expression::RegExpLiteral(literal) = expr else { return };
@ -43,26 +68,4 @@ impl<'a> RegexpFlags<'a> {
arguments.push(Argument::Expression(flags_literal));
*expr = self.ast.new_expression(Span::default(), callee, arguments, None);
}
fn from_transform_target(value: TransformTarget) -> RegExpFlags {
let mut flag = RegExpFlags::empty();
if value < TransformTarget::ES2015 {
flag |= RegExpFlags::Y;
flag |= RegExpFlags::U;
}
if value < TransformTarget::ES2018 {
flag |= RegExpFlags::S;
}
if value < TransformTarget::ES2022 {
flag |= RegExpFlags::D;
}
if value < TransformTarget::ES2024 {
flag |= RegExpFlags::V;
}
if value < TransformTarget::ESNext {
flag |= RegExpFlags::I;
flag |= RegExpFlags::M;
}
flag
}
}

View file

@ -11,7 +11,7 @@ use oxc::{
parser::{Parser, ParserReturn},
semantic::{SemanticBuilder, SemanticBuilderReturn},
span::SourceType,
transformer::{Assumptions, TransformOptions, TransformTarget, Transformer},
transformer::{TransformOptions, TransformTarget, Transformer},
};
use oxc_linter::{LintContext, Linter};
use oxc_query::{schema, Adapter, SCHEMA_TEXT};
@ -215,11 +215,8 @@ impl Oxc {
let semantic = SemanticBuilder::new(source_text, source_type).build(program).semantic;
let (symbols, _scope_tree) = semantic.into_symbol_table_and_scope_tree();
let symbols = Rc::new(RefCell::new(symbols));
let options = TransformOptions {
target: TransformTarget::ES2015,
react: None,
assumptions: Assumptions::default(),
};
let options =
TransformOptions { target: TransformTarget::ES2015, ..TransformOptions::default() };
Transformer::new(&allocator, source_type, &symbols, options).build(program);
}

View file

@ -9,7 +9,11 @@ license.workspace = true
doctest = false
[dependencies]
oxc_syntax = { workspace = true, features = ["serde"] }
project-root = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
ureq = { workspace = true }
url = { workspace = true }

94
tasks/common/src/babel.rs Normal file
View file

@ -0,0 +1,94 @@
use std::path::Path;
use oxc_syntax::assumptions::CompilerAssumptions;
use serde::Deserialize;
use serde_json::Value;
/// Babel options.json for tests
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BabelOptions {
pub source_type: Option<String>,
pub throws: Option<String>,
#[serde(default)]
pub plugins: Vec<Value>, // Can be a string or an array
#[serde(default)]
pub allow_return_outside_function: bool,
#[serde(default)]
pub allow_await_outside_function: bool,
#[serde(default)]
pub allow_undeclared_exports: bool,
#[serde(default)]
pub assumptions: CompilerAssumptions,
}
impl BabelOptions {
/// Read options.json and merge them with options.json from ancestors directories.
/// # Panics
pub fn from_path(path: &Path) -> Self {
let mut options_json: Option<Self> = None;
for path in path.ancestors().take(3) {
let file = path.join("options.json");
if !file.exists() {
continue;
}
let file = std::fs::read_to_string(&file).unwrap();
let new_json: Self = serde_json::from_str(&file).unwrap();
if let Some(existing_json) = options_json.as_mut() {
if let Some(source_type) = new_json.source_type {
existing_json.source_type = Some(source_type);
}
if let Some(throws) = new_json.throws {
existing_json.throws = Some(throws);
}
existing_json.plugins.extend(new_json.plugins);
} else {
options_json = Some(new_json);
}
}
options_json.unwrap_or_default()
}
pub fn is_jsx(&self) -> bool {
self.plugins.iter().any(|v| v.as_str().is_some_and(|v| v == "jsx"))
}
pub fn is_typescript(&self) -> bool {
self.plugins.iter().any(|v| {
let string_value = v.as_str().is_some_and(|v| v == "typescript");
let array_value = v.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
string_value || array_value
})
}
pub fn is_typescript_definition(&self) -> bool {
self.plugins.iter().filter_map(Value::as_array).any(|p| {
let typescript = p.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
let dts = p
.get(1)
.and_then(Value::as_object)
.and_then(|v| v.get("dts"))
.and_then(Value::as_bool)
.is_some_and(|v| v);
typescript && dts
})
}
pub fn is_module(&self) -> bool {
self.source_type.as_ref().map_or(false, |s| matches!(s.as_str(), "module" | "unambiguous"))
}
/// Returns
/// * `Some<None>` if the plugin exists without a config
/// * `Some<Some<Value>>` if the plugin exists with a config
/// * `None` if the plugin does not exist
pub fn get_plugin(&self, name: &str) -> Option<Option<Value>> {
self.plugins.iter().find_map(|v| match v {
Value::String(s) if s == name => Some(None),
Value::Array(a) if a.get(0).and_then(Value::as_str).is_some_and(|s| s == name) => {
Some(a.get(1).cloned())
}
_ => None,
})
}
}

View file

@ -1,10 +1,10 @@
use std::path::{Path, PathBuf};
mod babel;
mod request;
mod test_file;
pub use self::request::agent;
pub use self::test_file::*;
pub use crate::{babel::BabelOptions, request::agent, test_file::*};
/// # Panics
/// Invalid Project Root

View file

@ -1,9 +1,11 @@
use std::path::{Path, PathBuf};
use oxc_span::SourceType;
use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value;
use oxc_span::SourceType;
use oxc_tasks_common::BabelOptions;
use crate::{
project_root,
suite::{Case, Suite, TestResult},
@ -17,53 +19,6 @@ pub struct BabelOutput {
pub errors: Option<Vec<String>>,
}
/// options.json
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BabelOptions {
source_type: Option<String>,
throws: Option<String>,
#[serde(default)]
plugins: Vec<Value>, // Can be a string or an array
#[serde(default)]
allow_return_outside_function: bool,
#[serde(default)]
allow_await_outside_function: bool,
#[serde(default)]
allow_undeclared_exports: bool,
}
impl BabelOptions {
fn is_jsx(&self) -> bool {
self.plugins.iter().any(|v| v.as_str().is_some_and(|v| v == "jsx"))
}
fn is_typescript(&self) -> bool {
self.plugins.iter().any(|v| {
let string_value = v.as_str().is_some_and(|v| v == "typescript");
let array_value = v.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
string_value || array_value
})
}
fn is_typescript_definition(&self) -> bool {
self.plugins.iter().filter_map(Value::as_array).any(|p| {
let typescript = p.get(0).and_then(Value::as_str).is_some_and(|s| s == "typescript");
let dts = p
.get(1)
.and_then(Value::as_object)
.and_then(|v| v.get("dts"))
.and_then(Value::as_bool)
.is_some_and(|v| v);
typescript && dts
})
}
fn is_module(&self) -> bool {
self.source_type.as_ref().map_or(false, |s| matches!(s.as_str(), "module" | "unambiguous"))
}
}
pub struct BabelSuite<T: Case> {
test_root: PathBuf,
test_cases: Vec<T>,
@ -147,28 +102,6 @@ impl BabelCase {
Self::read_file::<BabelOutput>(&dir, "output.extended.json")
}
/// read options.json, it exists in ancestor folders as well and they need to be merged
fn read_options_json(path: &Path) -> BabelOptions {
let dir = project_root().join(FIXTURES_PATH).join(path);
let mut options_json: Option<BabelOptions> = None;
for path in dir.ancestors().take(3) {
if let Some(new_json) = Self::read_file::<BabelOptions>(path, "options.json") {
if let Some(existing_json) = options_json.as_mut() {
if let Some(source_type) = new_json.source_type {
existing_json.source_type = Some(source_type);
}
if let Some(throws) = new_json.throws {
existing_json.throws = Some(throws);
}
existing_json.plugins.extend(new_json.plugins);
} else {
options_json = Some(new_json);
}
}
}
options_json.unwrap_or_default()
}
// it is an error if:
// * its output.json contains an errors field
// * the directory contains a options.json with a "throws" field
@ -191,7 +124,8 @@ impl BabelCase {
impl Case for BabelCase {
/// # Panics
fn new(path: PathBuf, code: String) -> Self {
let options = Self::read_options_json(&path);
let dir = project_root().join(FIXTURES_PATH).join(&path);
let options = BabelOptions::from_path(dir.parent().unwrap());
let source_type = SourceType::from_path(&path)
.unwrap()
.with_script(true)

View file

@ -15,6 +15,7 @@ doctest = false
[dependencies]
oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
oxc_allocator = { workspace = true }
oxc_parser = { workspace = true }
oxc_semantic = { workspace = true }
@ -22,8 +23,10 @@ oxc_codegen = { workspace = true }
oxc_transformer = { workspace = true }
oxc_tasks_common = { workspace = true }
walkdir = { workspace = true }
pico-args = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
walkdir = { workspace = true }
pico-args = { workspace = true }
[target.'cfg(not(target_env = "msvc"))'.dependencies]
jemallocator = { workspace = true }

View file

@ -1,4 +1,4 @@
Passed: 95/1091
Passed: 104/1091
# babel-plugin-transform-unicode-sets-regex (0/4)
* Failed: basic/basic/input.js
@ -491,11 +491,8 @@ Passed: 95/1091
* Failed: to-native-fields/static-shadow/input.js
* Failed: to-native-fields/static-shadowed-binding/input.js
# babel-plugin-transform-logical-assignment-operators (0/6)
* Failed: logical-assignment/anonymous-functions-transform/input.js
* Failed: logical-assignment/arrow-functions-transform/input.js
# babel-plugin-transform-logical-assignment-operators (3/6)
* Failed: logical-assignment/general-semantics/input.js
* Failed: logical-assignment/named-functions-transform/input.js
* Failed: logical-assignment/null-coalescing/input.js
* Failed: logical-assignment/null-coalescing-without-other/input.js
@ -535,15 +532,9 @@ Passed: 95/1091
* Failed: export-namespace/namespace-string/input.mjs
* Failed: export-namespace/namespace-typescript/input.mjs
# babel-plugin-transform-nullish-coalescing-operator (4/12)
* Failed: assumption-noDocumentAll/transform/input.js
* Failed: assumption-noDocumentAll/transform-in-default-destructuring/input.js
# babel-plugin-transform-nullish-coalescing-operator (10/12)
* Failed: assumption-noDocumentAll/transform-in-default-param/input.js
* Failed: assumption-noDocumentAll/transform-in-function/input.js
* Failed: assumption-noDocumentAll/transform-static-refs-in-default/input.js
* Failed: assumption-noDocumentAll/transform-static-refs-in-function/input.js
* Failed: nullish-coalescing/transform-in-default-param/input.js
* Failed: nullish-coalescing/transform-loose/input.js
# babel-plugin-transform-optional-chaining (1/46)
* Failed: assumption-noDocumentAll/assignment/input.js

View file

@ -1,8 +1,10 @@
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::{
cell::RefCell,
fs::{self, File},
io::Write,
path::{Path, PathBuf},
path::PathBuf,
rc::Rc,
};
use walkdir::WalkDir;
@ -12,9 +14,10 @@ use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::{SourceType, VALID_EXTENSIONS};
use oxc_tasks_common::{normalize_path, project_root};
use oxc_tasks_common::{normalize_path, project_root, BabelOptions};
use oxc_transformer::{
Assumptions, TransformOptions, TransformReactOptions, TransformTarget, Transformer,
NullishCoalescingOperatorOptions, ReactJsxOptions, TransformOptions, TransformTarget,
Transformer,
};
#[test]
@ -24,163 +27,216 @@ fn test() {
}
#[derive(Default)]
pub struct BabelOptions {
pub struct TestRunnerOptions {
pub filter: Option<String>,
}
/// # Panics
pub fn babel(options: &BabelOptions) {
let root = project_root().join("tasks/coverage/babel/packages");
/// The test runner which walks the babel repository and searches for transformation tests.
pub struct TestRunner {
options: TestRunnerOptions,
}
let cases = [
// ES2024
"babel-plugin-transform-unicode-sets-regex",
// ES2022
"babel-plugin-transform-class-properties",
"babel-plugin-transform-class-static-block",
"babel-plugin-transform-private-methods",
"babel-plugin-transform-private-property-in-object",
// [Syntax] "babel-plugin-transform-syntax-top-level-await",
// ES2021
"babel-plugin-transform-logical-assignment-operators",
"babel-plugin-transform-numeric-separator",
// ES2020
"babel-plugin-transform-export-namespace-from",
"babel-plugin-transform-dynamic-import",
"babel-plugin-transform-export-namespace-from",
"babel-plugin-transform-nullish-coalescing-operator",
"babel-plugin-transform-optional-chaining",
// [Syntax] "babel-plugin-transform-syntax-bigint",
// [Syntax] "babel-plugin-transform-syntax-dynamic-import",
// [Syntax] "babel-plugin-transform-syntax-import-meta",
// ES2019
"babel-plugin-transform-optional-catch-binding",
"babel-plugin-transform-json-strings",
// ES2018
"babel-plugin-transform-async-generator-functions",
"babel-plugin-transform-object-rest-spread",
// [Regex] "babel-plugin-transform-unicode-property-regex",
"babel-plugin-transform-dotall-regex",
// [Regex] "babel-plugin-transform-named-capturing-groups-regex",
// ES2017
"babel-plugin-transform-async-to-generator",
// ES2016
"babel-plugin-transform-exponentiation-operator",
// ES2015
"babel-plugin-transform-shorthand-properties",
"babel-plugin-transform-sticky-regex",
"babel-plugin-transform-unicode-regex",
// TypeScript
"babel-plugin-transform-typescript",
// React
"babel-plugin-transform-react-jsx",
];
fn root() -> PathBuf {
project_root().join("tasks/coverage/babel/packages")
}
let mut snapshot = String::new();
let mut total = 0;
let mut all_passed = 0;
impl TestRunner {
pub fn new(options: TestRunnerOptions) -> Self {
Self { options }
}
// Get all fixtures
for case in cases {
let root = root.join(case).join("test/fixtures");
let mut paths = WalkDir::new(&root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| {
e.path().file_stem().is_some_and(|name| name == "input")
&& e.path()
.extension()
.is_some_and(|ext| VALID_EXTENSIONS.contains(&ext.to_str().unwrap()))
})
.map(walkdir::DirEntry::into_path)
.collect::<Vec<_>>();
paths.sort_unstable();
let num_of_tests = paths.len();
total += num_of_tests;
/// # Panics
pub fn run(self) {
let root = root();
// Run the test
let (passed, failed): (Vec<PathBuf>, Vec<PathBuf>) =
paths.into_iter().partition(|path| babel_test(path, options));
all_passed += passed.len();
let cases = [
// ES2024
"babel-plugin-transform-unicode-sets-regex",
// ES2022
"babel-plugin-transform-class-properties",
"babel-plugin-transform-class-static-block",
"babel-plugin-transform-private-methods",
"babel-plugin-transform-private-property-in-object",
// [Syntax] "babel-plugin-transform-syntax-top-level-await",
// ES2021
"babel-plugin-transform-logical-assignment-operators",
"babel-plugin-transform-numeric-separator",
// ES2020
"babel-plugin-transform-export-namespace-from",
"babel-plugin-transform-dynamic-import",
"babel-plugin-transform-export-namespace-from",
"babel-plugin-transform-nullish-coalescing-operator",
"babel-plugin-transform-optional-chaining",
// [Syntax] "babel-plugin-transform-syntax-bigint",
// [Syntax] "babel-plugin-transform-syntax-dynamic-import",
// [Syntax] "babel-plugin-transform-syntax-import-meta",
// ES2019
"babel-plugin-transform-optional-catch-binding",
"babel-plugin-transform-json-strings",
// ES2018
"babel-plugin-transform-async-generator-functions",
"babel-plugin-transform-object-rest-spread",
// [Regex] "babel-plugin-transform-unicode-property-regex",
"babel-plugin-transform-dotall-regex",
// [Regex] "babel-plugin-transform-named-capturing-groups-regex",
// ES2017
"babel-plugin-transform-async-to-generator",
// ES2016
"babel-plugin-transform-exponentiation-operator",
// ES2015
"babel-plugin-transform-shorthand-properties",
"babel-plugin-transform-sticky-regex",
"babel-plugin-transform-unicode-regex",
// TypeScript
"babel-plugin-transform-typescript",
// React
"babel-plugin-transform-react-jsx",
];
// Snapshot
snapshot.push_str("# ");
snapshot.push_str(case);
if failed.is_empty() {
snapshot.push_str(" (All passed)\n");
} else {
snapshot.push_str(&format!(" ({}/{})\n", passed.len(), num_of_tests));
}
for path in failed {
snapshot.push_str("* Failed: ");
snapshot.push_str(&normalize_path(path.strip_prefix(&root).unwrap()));
let mut snapshot = String::new();
let mut total = 0;
let mut all_passed = 0;
// Get all fixtures
for case in cases {
let root = root.join(case).join("test/fixtures");
let mut paths = WalkDir::new(&root)
.into_iter()
.filter_map(Result::ok)
.filter(|e| {
e.path().file_stem().is_some_and(|name| name == "input")
&& e.path()
.extension()
.is_some_and(|ext| VALID_EXTENSIONS.contains(&ext.to_str().unwrap()))
})
.map(walkdir::DirEntry::into_path)
.collect::<Vec<_>>();
paths.sort_unstable();
let num_of_tests = paths.len();
total += num_of_tests;
// Run the test
let (passed, failed): (Vec<PathBuf>, Vec<PathBuf>) = paths
.into_iter()
.partition(|path| TestCase::new(path).test(self.options.filter.as_deref()));
all_passed += passed.len();
// Snapshot
snapshot.push_str("# ");
snapshot.push_str(case);
if failed.is_empty() {
snapshot.push_str(" (All passed)\n");
} else {
snapshot.push_str(&format!(" ({}/{})\n", passed.len(), num_of_tests));
}
for path in failed {
snapshot.push_str("* Failed: ");
snapshot.push_str(&normalize_path(path.strip_prefix(&root).unwrap()));
snapshot.push('\n');
}
snapshot.push('\n');
}
snapshot.push('\n');
}
let snapshot = format!("Passed: {all_passed}/{total}\n\n{snapshot}");
let path = project_root().join("tasks/transform_conformance/babel.snap.md");
let mut file = File::create(path).unwrap();
file.write_all(snapshot.as_bytes()).unwrap();
let snapshot = format!("Passed: {all_passed}/{total}\n\n{snapshot}");
let path = project_root().join("tasks/transform_conformance/babel.snap.md");
let mut file = File::create(path).unwrap();
file.write_all(snapshot.as_bytes()).unwrap();
}
}
/// Test conformance by comparing the parsed babel code and transformed code.
fn babel_test(input_path: &Path, options: &BabelOptions) -> bool {
let output_path = input_path.parent().unwrap().read_dir().unwrap().find_map(|entry| {
let path = entry.ok()?.path();
let file_stem = path.file_stem()?;
(file_stem == "output").then_some(path)
});
let source_text = fs::read_to_string(input_path).unwrap();
let filtered =
options.filter.as_ref().is_some_and(|f| input_path.to_string_lossy().as_ref().contains(f));
if filtered {
println!("input_path: {input_path:?}");
println!("output_path: {output_path:?}");
}
let allocator = Allocator::default();
let source_type = SourceType::from_path(input_path).unwrap();
// Get expected code by parsing the source text, so we can get the same code generated result.
let expected = output_path.and_then(|path| fs::read_to_string(path).ok());
let Some(expected) = &expected else { return false };
let expected_program = Parser::new(&allocator, expected, source_type).parse().program;
let expected_code =
Codegen::<false>::new(source_text.len(), CodegenOptions).build(&expected_program);
// Get transformed text.
let transformed_program = Parser::new(&allocator, &source_text, source_type).parse().program;
let transform_options = TransformOptions {
target: TransformTarget::ES5,
react: Some(TransformReactOptions::default()),
assumptions: Assumptions::default(),
};
let semantic =
SemanticBuilder::new(&source_text, source_type).build(&transformed_program).semantic;
let (symbols, _scope_tree) = semantic.into_symbol_table_and_scope_tree();
let symbols = Rc::new(RefCell::new(symbols));
let transformed_program = allocator.alloc(transformed_program);
Transformer::new(&allocator, source_type, &symbols, transform_options)
.build(transformed_program);
let transformed_code =
Codegen::<false>::new(source_text.len(), CodegenOptions).build(transformed_program);
let passed = transformed_code == expected_code;
if filtered {
println!("Expected:\n");
println!("{expected}\n");
println!("Expected codegen:\n");
println!("{expected_code}\n");
println!("Transformed:\n");
println!("{transformed_code}\n");
println!("Passed: {passed}");
}
passed
struct TestCase {
path: PathBuf,
options: BabelOptions,
}
impl TestCase {
fn new<P: Into<PathBuf>>(path: P) -> Self {
let path = path.into();
let options = BabelOptions::from_path(path.parent().unwrap());
Self { path, options }
}
fn transform_options(&self) -> TransformOptions {
fn get_options<T: Default + DeserializeOwned>(value: Option<Value>) -> T {
value.and_then(|v| serde_json::from_value::<T>(v).ok()).unwrap_or_default()
}
let options = &self.options;
TransformOptions {
target: TransformTarget::ESNext,
react_jsx: Some(ReactJsxOptions::default()),
assumptions: options.assumptions,
class_static_block: options.get_plugin("transform-class-static-block").is_some(),
logical_assignment_operators: options
.get_plugin("transform-logical-assignment-operators")
.is_some(),
nullish_coalescing_operator: self
.options
.get_plugin("transform-nullish-coalescing-operator")
.map(get_options::<NullishCoalescingOperatorOptions>),
optional_catch_binding: options
.get_plugin("transform-optional-catch-binding")
.is_some(),
exponentiation_operator: options
.get_plugin("transform-exponentiation-operator")
.is_some(),
shorthand_properties: options.get_plugin("transform-shorthand-properties").is_some(),
sticky_regex: options.get_plugin("transform-sticky-regex").is_some(),
}
}
/// Test conformance by comparing the parsed babel code and transformed code.
fn test(&self, filter: Option<&str>) -> bool {
let output_path = self.path.parent().unwrap().read_dir().unwrap().find_map(|entry| {
let path = entry.ok()?.path();
let file_stem = path.file_stem()?;
(file_stem == "output").then_some(path)
});
let source_text = fs::read_to_string(&self.path).unwrap();
let filtered = filter.is_some_and(|f| self.path.to_string_lossy().as_ref().contains(f));
if filtered {
println!("input_path: {:?}", &self.path);
println!("output_path: {output_path:?}");
}
let allocator = Allocator::default();
let source_type = SourceType::from_path(&self.path).unwrap();
// Get expected code by parsing the source text, so we can get the same code generated result.
let expected = output_path.and_then(|path| fs::read_to_string(path).ok());
let Some(expected) = &expected else { return false };
let expected_program = Parser::new(&allocator, expected, source_type).parse().program;
let expected_code =
Codegen::<false>::new(source_text.len(), CodegenOptions).build(&expected_program);
// Get transformed text.
let transformed_program =
Parser::new(&allocator, &source_text, source_type).parse().program;
let semantic =
SemanticBuilder::new(&source_text, source_type).build(&transformed_program).semantic;
let (symbols, _scope_tree) = semantic.into_symbol_table_and_scope_tree();
let symbols = Rc::new(RefCell::new(symbols));
let transformed_program = allocator.alloc(transformed_program);
Transformer::new(&allocator, source_type, &symbols, self.transform_options())
.build(transformed_program);
let transformed_code =
Codegen::<false>::new(source_text.len(), CodegenOptions).build(transformed_program);
let passed = transformed_code == expected_code;
if filtered {
println!("Expected:\n");
println!("{expected}\n");
println!("Expected codegen:\n");
println!("{expected_code}\n");
println!("Transformed:\n");
println!("{transformed_code}\n");
println!("Passed: {passed}");
}
passed
}
}

View file

@ -6,13 +6,13 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use oxc_transform_conformance::{babel, BabelOptions};
use oxc_transform_conformance::{TestRunner, TestRunnerOptions};
use pico_args::Arguments;
fn main() {
let mut args = Arguments::from_env();
let options = BabelOptions { filter: args.opt_value_from_str("--filter").unwrap() };
let options = TestRunnerOptions { filter: args.opt_value_from_str("--filter").unwrap() };
babel(&options);
TestRunner::new(options).run();
}