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

View file

@ -15,10 +15,6 @@ categories.workspace = true
[lib] [lib]
doctest = false doctest = false
[features]
default = []
serde = ["dep:serde"]
[dependencies] [dependencies]
oxc_index = { workspace = true } oxc_index = { workspace = true }
oxc_span = { workspace = true } oxc_span = { workspace = true }
@ -29,3 +25,7 @@ bitflags = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
dashmap = { 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 //! Common code for JavaScript Syntax
pub mod assumptions;
pub mod identifier; pub mod identifier;
pub mod module_record; pub mod module_record;
pub mod operator; pub mod operator;

View file

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

View file

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

View file

@ -1,7 +1,9 @@
use std::rc::Rc;
use oxc_ast::{ast::*, AstBuilder}; use oxc_ast::{ast::*, AstBuilder};
use oxc_span::GetSpan; use oxc_span::GetSpan;
use std::rc::Rc; use crate::options::{TransformOptions, TransformTarget};
/// ES2015: Shorthand Properties /// ES2015: Shorthand Properties
/// ///
@ -13,8 +15,9 @@ pub struct ShorthandProperties<'a> {
} }
impl<'a> ShorthandProperties<'a> { impl<'a> ShorthandProperties<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self { pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
Self { ast } (options.target < TransformTarget::ES2015 || options.shorthand_properties)
.then(|| Self { ast })
} }
pub fn transform_object_property<'b>(&mut self, obj_prop: &'b mut ObjectProperty<'a>) { 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_span::{Atom, Span};
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator}; use oxc_syntax::operator::{AssignmentOperator, BinaryOperator};
use crate::utils::CreateVars; use crate::{
options::{TransformOptions, TransformTarget},
utils::CreateVars,
};
/// ES2016: Exponentiation Operator /// ES2016: Exponentiation Operator
/// ///
@ -36,9 +39,15 @@ impl<'a> CreateVars<'a> for ExponentiationOperator<'a> {
} }
impl<'a> ExponentiationOperator<'a> { impl<'a> ExponentiationOperator<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, symbols: Rc<RefCell<SymbolTable>>) -> Self { 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(); let vars = ast.new_vec();
Self { ast, symbols, vars } Self { ast, symbols, vars }
})
} }
pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { 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_ast::{ast::*, AstBuilder};
use oxc_span::Span; use oxc_span::Span;
use std::rc::Rc; use crate::options::{TransformOptions, TransformTarget};
/// ES2019: Optional Catch Binding /// ES2019: Optional Catch Binding
/// ///
@ -13,8 +15,9 @@ pub struct OptionalCatchBinding<'a> {
} }
impl<'a> OptionalCatchBinding<'a> { impl<'a> OptionalCatchBinding<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self { pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
Self { ast } (options.target < TransformTarget::ES2019 || options.optional_catch_binding)
.then(|| Self { ast })
} }
pub fn transform_catch_clause<'b>(&mut self, clause: &'b mut CatchClause<'a>) { pub fn transform_catch_clause<'b>(&mut self, clause: &'b mut CatchClause<'a>) {

View file

@ -1,3 +1,5 @@
mod nullish_coalescing_operator; 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 std::{cell::RefCell, rc::Rc};
use oxc_allocator::Vec; use oxc_allocator::Vec;
@ -6,7 +7,15 @@ use oxc_semantic::SymbolTable;
use oxc_span::Span; use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, BinaryOperator, LogicalOperator}; 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 /// 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://babeljs.io/docs/babel-plugin-transform-nullish-coalescing-operator>
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-nullish-coalescing-operator> /// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-nullish-coalescing-operator>
pub struct NullishCoalescingOperator<'a> { pub struct NullishCoalescingOperator<'a> {
no_document_all: bool,
ast: Rc<AstBuilder<'a>>, ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>, symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions,
vars: Vec<'a, VariableDeclarator<'a>>, vars: Vec<'a, VariableDeclarator<'a>>,
} }
@ -34,10 +44,15 @@ impl<'a> NullishCoalescingOperator<'a> {
pub fn new( pub fn new(
ast: Rc<AstBuilder<'a>>, ast: Rc<AstBuilder<'a>>,
symbols: Rc<RefCell<SymbolTable>>, symbols: Rc<RefCell<SymbolTable>>,
assumptions: Assumptions, options: &TransformOptions,
) -> Self { ) -> 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(); let vars = ast.new_vec();
Self { ast, symbols, assumptions, vars } Self { no_document_all, ast, symbols, vars }
})
} }
pub fn transform_expression(&mut self, expr: &mut Expression<'a>) { 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); 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)); let null = self.ast.literal_null_expression(NullLiteral::new(span));
self.ast.binary_expression(span, assignment, BinaryOperator::Inequality, null) self.ast.binary_expression(span, assignment, BinaryOperator::Inequality, null)
} else { } else {

View file

@ -1,8 +1,10 @@
use std::rc::Rc;
use oxc_ast::{ast::*, AstBuilder}; use oxc_ast::{ast::*, AstBuilder};
use oxc_span::Span; use oxc_span::Span;
use oxc_syntax::operator::{AssignmentOperator, LogicalOperator}; use oxc_syntax::operator::{AssignmentOperator, LogicalOperator};
use std::rc::Rc; use crate::options::{TransformOptions, TransformTarget};
/// ES2021: Logical Assignment Operators /// ES2021: Logical Assignment Operators
/// ///
@ -14,8 +16,9 @@ pub struct LogicalAssignmentOperators<'a> {
} }
impl<'a> LogicalAssignmentOperators<'a> { impl<'a> LogicalAssignmentOperators<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self { pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
Self { ast } (options.target < TransformTarget::ES2021 || options.logical_assignment_operators)
.then(|| Self { ast })
} }
pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) { 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_ast::{ast::*, AstBuilder};
use oxc_span::{Atom, Span}; use oxc_span::{Atom, Span};
use std::{collections::HashSet, rc::Rc}; use crate::options::{TransformOptions, TransformTarget};
/// ES2022: Class Static Block /// ES2022: Class Static Block
/// ///
@ -13,8 +15,9 @@ pub struct ClassStaticBlock<'a> {
} }
impl<'a> ClassStaticBlock<'a> { impl<'a> ClassStaticBlock<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self { pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
Self { ast } (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>) { 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::{ use crate::{
es2015::ShorthandProperties, es2016::ExponentiationOperator, es2019::OptionalCatchBinding, es2015::ShorthandProperties, es2016::ExponentiationOperator, es2019::OptionalCatchBinding,
es2020::NullishCoalescingOperator, es2021::LogicalAssignmentOperators, react_jsx::ReactJsx, es2020::NullishCoalescingOperator, es2021::LogicalAssignmentOperators,
regexp::RegexpFlags, typescript::TypeScript, utils::CreateVars, es2022::ClassStaticBlock, react_jsx::ReactJsx, regexp::RegexpFlags, typescript::TypeScript,
utils::CreateVars,
}; };
pub use crate::options::{ pub use crate::{
Assumptions, TransformOptions, TransformReactOptions, TransformReactRuntime, TransformTarget, es2020::NullishCoalescingOperatorOptions,
options::{TransformOptions, TransformTarget},
react_jsx::{ReactJsxOptions, ReactJsxRuntime},
}; };
#[derive(Default)] #[derive(Default)]
@ -46,7 +49,7 @@ pub struct Transformer<'a> {
react_jsx: Option<ReactJsx<'a>>, react_jsx: Option<ReactJsx<'a>>,
regexp_flags: Option<RegexpFlags<'a>>, regexp_flags: Option<RegexpFlags<'a>>,
// es2022 // es2022
es2022_class_static_block: Option<es2022::ClassStaticBlock<'a>>, es2022_class_static_block: Option<ClassStaticBlock<'a>>,
// es2021 // es2021
es2021_logical_assignment_operators: Option<LogicalAssignmentOperators<'a>>, es2021_logical_assignment_operators: Option<LogicalAssignmentOperators<'a>>,
// es2020 // es2020
@ -70,14 +73,14 @@ impl<'a> Transformer<'a> {
let ast = Rc::new(AstBuilder::new(allocator)); let ast = Rc::new(AstBuilder::new(allocator));
Self { Self {
typescript: source_type.is_typescript().then(|| TypeScript::new(Rc::clone(&ast))), typescript: source_type.is_typescript().then(|| TypeScript::new(Rc::clone(&ast))),
react_jsx: options.react.map(|options| ReactJsx::new(Rc::clone(&ast), options)), react_jsx: options.react_jsx.map(|options| ReactJsx::new(Rc::clone(&ast), options)),
regexp_flags: RegexpFlags::new(Rc::clone(&ast), options.target), regexp_flags: RegexpFlags::new(Rc::clone(&ast), &options),
es2022_class_static_block: (options.target < TransformTarget::ES2022).then(|| es2022::ClassStaticBlock::new(Rc::clone(&ast))), es2022_class_static_block: es2022::ClassStaticBlock::new(Rc::clone(&ast), &options),
es2021_logical_assignment_operators: (options.target < TransformTarget::ES2021).then(|| LogicalAssignmentOperators::new(Rc::clone(&ast))), es2021_logical_assignment_operators: LogicalAssignmentOperators::new(Rc::clone(&ast), &options),
es2020_nullish_coalescing_operators: (options.target < TransformTarget::ES2020).then(|| NullishCoalescingOperator::new(Rc::clone(&ast), Rc::clone(symbols), options.assumptions)), es2020_nullish_coalescing_operators: NullishCoalescingOperator::new(Rc::clone(&ast), Rc::clone(symbols), &options),
es2019_optional_catch_binding: (options.target < TransformTarget::ES2019).then(|| OptionalCatchBinding::new(Rc::clone(&ast))), es2019_optional_catch_binding: OptionalCatchBinding::new(Rc::clone(&ast), &options),
es2016_exponentiation_operator: (options.target < TransformTarget::ES2016).then(|| ExponentiationOperator::new(Rc::clone(&ast), Rc::clone(symbols))), es2016_exponentiation_operator: ExponentiationOperator::new(Rc::clone(&ast), Rc::clone(symbols), &options),
es2015_shorthand_properties: (options.target < TransformTarget::ES2015).then(|| ShorthandProperties::new(Rc::clone(&ast))), 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)] #[derive(Debug, Default, Clone, Copy)]
pub struct TransformOptions { pub struct TransformOptions {
pub target: TransformTarget, pub target: TransformTarget,
pub react: Option<TransformReactOptions>, pub assumptions: CompilerAssumptions,
pub assumptions: Assumptions,
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> /// See <https://www.typescriptlang.org/tsconfig#target>
@ -20,25 +39,3 @@ pub enum TransformTarget {
#[default] #[default]
ESNext, 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 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 /// Transform React JSX
/// ///
@ -11,11 +22,11 @@ use crate::TransformReactOptions;
/// * <https://github.com/babel/babel/tree/main/packages/babel-helper-builder-react-jsx> /// * <https://github.com/babel/babel/tree/main/packages/babel-helper-builder-react-jsx>
pub struct ReactJsx<'a> { pub struct ReactJsx<'a> {
_ast: Rc<AstBuilder<'a>>, _ast: Rc<AstBuilder<'a>>,
_options: TransformReactOptions, _options: ReactJsxOptions,
} }
impl<'a> ReactJsx<'a> { 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 } Self { _ast, _options }
} }
} }

View file

@ -3,7 +3,7 @@ use oxc_span::{Atom, Span};
use std::rc::Rc; use std::rc::Rc;
use crate::TransformTarget; use crate::{TransformOptions, TransformTarget};
/// Transforms unsupported regex flags into Regex constructors. /// Transforms unsupported regex flags into Regex constructors.
/// ///
@ -20,11 +20,36 @@ pub struct RegexpFlags<'a> {
} }
impl<'a> RegexpFlags<'a> { impl<'a> RegexpFlags<'a> {
pub fn new(ast: Rc<AstBuilder<'a>>, transform_target: TransformTarget) -> Option<Self> { pub fn new(ast: Rc<AstBuilder<'a>>, options: &TransformOptions) -> Option<Self> {
let transform_flags = Self::from_transform_target(transform_target); let transform_flags = Self::from_transform_target(options);
(!transform_flags.is_empty()).then(|| Self { ast, transform_flags }) (!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')` // `/regex/flags` -> `new RegExp('regex', 'flags')`
pub fn transform_expression(&self, expr: &mut Expression<'a>) { pub fn transform_expression(&self, expr: &mut Expression<'a>) {
let Expression::RegExpLiteral(literal) = expr else { return }; let Expression::RegExpLiteral(literal) = expr else { return };
@ -43,26 +68,4 @@ impl<'a> RegexpFlags<'a> {
arguments.push(Argument::Expression(flags_literal)); arguments.push(Argument::Expression(flags_literal));
*expr = self.ast.new_expression(Span::default(), callee, arguments, None); *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}, parser::{Parser, ParserReturn},
semantic::{SemanticBuilder, SemanticBuilderReturn}, semantic::{SemanticBuilder, SemanticBuilderReturn},
span::SourceType, span::SourceType,
transformer::{Assumptions, TransformOptions, TransformTarget, Transformer}, transformer::{TransformOptions, TransformTarget, Transformer},
}; };
use oxc_linter::{LintContext, Linter}; use oxc_linter::{LintContext, Linter};
use oxc_query::{schema, Adapter, SCHEMA_TEXT}; 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 semantic = SemanticBuilder::new(source_text, source_type).build(program).semantic;
let (symbols, _scope_tree) = semantic.into_symbol_table_and_scope_tree(); let (symbols, _scope_tree) = semantic.into_symbol_table_and_scope_tree();
let symbols = Rc::new(RefCell::new(symbols)); let symbols = Rc::new(RefCell::new(symbols));
let options = TransformOptions { let options =
target: TransformTarget::ES2015, TransformOptions { target: TransformTarget::ES2015, ..TransformOptions::default() };
react: None,
assumptions: Assumptions::default(),
};
Transformer::new(&allocator, source_type, &symbols, options).build(program); Transformer::new(&allocator, source_type, &symbols, options).build(program);
} }

View file

@ -9,7 +9,11 @@ license.workspace = true
doctest = false doctest = false
[dependencies] [dependencies]
oxc_syntax = { workspace = true, features = ["serde"] }
project-root = { workspace = true } project-root = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
ureq = { workspace = true } ureq = { workspace = true }
url = { 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}; use std::path::{Path, PathBuf};
mod babel;
mod request; mod request;
mod test_file; mod test_file;
pub use self::request::agent; pub use crate::{babel::BabelOptions, request::agent, test_file::*};
pub use self::test_file::*;
/// # Panics /// # Panics
/// Invalid Project Root /// Invalid Project Root

View file

@ -1,9 +1,11 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use oxc_span::SourceType;
use serde::{de::DeserializeOwned, Deserialize}; use serde::{de::DeserializeOwned, Deserialize};
use serde_json::Value; use serde_json::Value;
use oxc_span::SourceType;
use oxc_tasks_common::BabelOptions;
use crate::{ use crate::{
project_root, project_root,
suite::{Case, Suite, TestResult}, suite::{Case, Suite, TestResult},
@ -17,53 +19,6 @@ pub struct BabelOutput {
pub errors: Option<Vec<String>>, 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> { pub struct BabelSuite<T: Case> {
test_root: PathBuf, test_root: PathBuf,
test_cases: Vec<T>, test_cases: Vec<T>,
@ -147,28 +102,6 @@ impl BabelCase {
Self::read_file::<BabelOutput>(&dir, "output.extended.json") 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: // it is an error if:
// * its output.json contains an errors field // * its output.json contains an errors field
// * the directory contains a options.json with a "throws" field // * the directory contains a options.json with a "throws" field
@ -191,7 +124,8 @@ impl BabelCase {
impl Case for BabelCase { impl Case for BabelCase {
/// # Panics /// # Panics
fn new(path: PathBuf, code: String) -> Self { 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) let source_type = SourceType::from_path(&path)
.unwrap() .unwrap()
.with_script(true) .with_script(true)

View file

@ -15,6 +15,7 @@ doctest = false
[dependencies] [dependencies]
oxc_span = { workspace = true } oxc_span = { workspace = true }
oxc_syntax = { workspace = true }
oxc_allocator = { workspace = true } oxc_allocator = { workspace = true }
oxc_parser = { workspace = true } oxc_parser = { workspace = true }
oxc_semantic = { workspace = true } oxc_semantic = { workspace = true }
@ -22,6 +23,8 @@ oxc_codegen = { workspace = true }
oxc_transformer = { workspace = true } oxc_transformer = { workspace = true }
oxc_tasks_common = { workspace = true } oxc_tasks_common = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }
walkdir = { workspace = true } walkdir = { workspace = true }
pico-args = { workspace = true } pico-args = { workspace = true }

View file

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

View file

@ -1,8 +1,10 @@
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::{ use std::{
cell::RefCell, cell::RefCell,
fs::{self, File}, fs::{self, File},
io::Write, io::Write,
path::{Path, PathBuf}, path::PathBuf,
rc::Rc, rc::Rc,
}; };
use walkdir::WalkDir; use walkdir::WalkDir;
@ -12,9 +14,10 @@ use oxc_codegen::{Codegen, CodegenOptions};
use oxc_parser::Parser; use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder; use oxc_semantic::SemanticBuilder;
use oxc_span::{SourceType, VALID_EXTENSIONS}; 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::{ use oxc_transformer::{
Assumptions, TransformOptions, TransformReactOptions, TransformTarget, Transformer, NullishCoalescingOperatorOptions, ReactJsxOptions, TransformOptions, TransformTarget,
Transformer,
}; };
#[test] #[test]
@ -24,13 +27,27 @@ fn test() {
} }
#[derive(Default)] #[derive(Default)]
pub struct BabelOptions { pub struct TestRunnerOptions {
pub filter: Option<String>, pub filter: Option<String>,
} }
/// # Panics /// The test runner which walks the babel repository and searches for transformation tests.
pub fn babel(options: &BabelOptions) { pub struct TestRunner {
let root = project_root().join("tasks/coverage/babel/packages"); options: TestRunnerOptions,
}
fn root() -> PathBuf {
project_root().join("tasks/coverage/babel/packages")
}
impl TestRunner {
pub fn new(options: TestRunnerOptions) -> Self {
Self { options }
}
/// # Panics
pub fn run(self) {
let root = root();
let cases = [ let cases = [
// ES2024 // ES2024
@ -99,8 +116,9 @@ pub fn babel(options: &BabelOptions) {
total += num_of_tests; total += num_of_tests;
// Run the test // Run the test
let (passed, failed): (Vec<PathBuf>, Vec<PathBuf>) = let (passed, failed): (Vec<PathBuf>, Vec<PathBuf>) = paths
paths.into_iter().partition(|path| babel_test(path, options)); .into_iter()
.partition(|path| TestCase::new(path).test(self.options.filter.as_deref()));
all_passed += passed.len(); all_passed += passed.len();
// Snapshot // Snapshot
@ -123,26 +141,67 @@ pub fn babel(options: &BabelOptions) {
let path = project_root().join("tasks/transform_conformance/babel.snap.md"); let path = project_root().join("tasks/transform_conformance/babel.snap.md");
let mut file = File::create(path).unwrap(); let mut file = File::create(path).unwrap();
file.write_all(snapshot.as_bytes()).unwrap(); file.write_all(snapshot.as_bytes()).unwrap();
}
} }
/// Test conformance by comparing the parsed babel code and transformed code. struct TestCase {
fn babel_test(input_path: &Path, options: &BabelOptions) -> bool { path: PathBuf,
let output_path = input_path.parent().unwrap().read_dir().unwrap().find_map(|entry| { 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 path = entry.ok()?.path();
let file_stem = path.file_stem()?; let file_stem = path.file_stem()?;
(file_stem == "output").then_some(path) (file_stem == "output").then_some(path)
}); });
let source_text = fs::read_to_string(input_path).unwrap(); let source_text = fs::read_to_string(&self.path).unwrap();
let filtered = let filtered = filter.is_some_and(|f| self.path.to_string_lossy().as_ref().contains(f));
options.filter.as_ref().is_some_and(|f| input_path.to_string_lossy().as_ref().contains(f));
if filtered { if filtered {
println!("input_path: {input_path:?}"); println!("input_path: {:?}", &self.path);
println!("output_path: {output_path:?}"); println!("output_path: {output_path:?}");
} }
let allocator = Allocator::default(); let allocator = Allocator::default();
let source_type = SourceType::from_path(input_path).unwrap(); 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. // 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 expected = output_path.and_then(|path| fs::read_to_string(path).ok());
@ -153,12 +212,8 @@ fn babel_test(input_path: &Path, options: &BabelOptions) -> bool {
// Get transformed text. // Get transformed text.
let transformed_program = Parser::new(&allocator, &source_text, source_type).parse().program; let transformed_program =
let transform_options = TransformOptions { Parser::new(&allocator, &source_text, source_type).parse().program;
target: TransformTarget::ES5,
react: Some(TransformReactOptions::default()),
assumptions: Assumptions::default(),
};
let semantic = let semantic =
SemanticBuilder::new(&source_text, source_type).build(&transformed_program).semantic; SemanticBuilder::new(&source_text, source_type).build(&transformed_program).semantic;
@ -167,7 +222,7 @@ fn babel_test(input_path: &Path, options: &BabelOptions) -> bool {
let transformed_program = allocator.alloc(transformed_program); let transformed_program = allocator.alloc(transformed_program);
Transformer::new(&allocator, source_type, &symbols, transform_options) Transformer::new(&allocator, source_type, &symbols, self.transform_options())
.build(transformed_program); .build(transformed_program);
let transformed_code = let transformed_code =
Codegen::<false>::new(source_text.len(), CodegenOptions).build(transformed_program); Codegen::<false>::new(source_text.len(), CodegenOptions).build(transformed_program);
@ -183,4 +238,5 @@ fn babel_test(input_path: &Path, options: &BabelOptions) -> bool {
println!("Passed: {passed}"); println!("Passed: {passed}");
} }
passed passed
}
} }

View file

@ -6,13 +6,13 @@ static GLOBAL: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[global_allocator] #[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
use oxc_transform_conformance::{babel, BabelOptions}; use oxc_transform_conformance::{TestRunner, TestRunnerOptions};
use pico_args::Arguments; use pico_args::Arguments;
fn main() { fn main() {
let mut args = Arguments::from_env(); 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();
} }