mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 12:51:57 +00:00
feat(transformer): transformer prototype (#918)
This PR introduces the oxc transformer. The code is purposely arranged without any Rust trickery such as traits and macros. I intend to keep the code style this way until something blows up. The next steps are: * unit tests * conformance tests with TypeScript and Babel * benchmarks
This commit is contained in:
parent
91784515f0
commit
419d5aa6ee
11 changed files with 250 additions and 1 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -1718,6 +1718,18 @@ dependencies = [
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "oxc_transformer"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"oxc_allocator",
|
||||||
|
"oxc_ast",
|
||||||
|
"oxc_formatter",
|
||||||
|
"oxc_parser",
|
||||||
|
"oxc_span",
|
||||||
|
"oxc_syntax",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "oxc_type_synthesis"
|
name = "oxc_type_synthesis"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,12 @@ pub struct IdentifierName {
|
||||||
pub name: Atom,
|
pub name: Atom,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl IdentifierName {
|
||||||
|
pub fn new(span: Span, name: Atom) -> Self {
|
||||||
|
Self { span, name }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Identifier Reference
|
/// Identifier Reference
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))]
|
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))]
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,12 @@ impl Hash for NullLiteral {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl NullLiteral {
|
||||||
|
pub fn new(span: Span) -> Self {
|
||||||
|
Self { span }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))]
|
#[cfg_attr(feature = "serde", derive(Serialize), serde(tag = "type"))]
|
||||||
pub struct NumberLiteral<'a> {
|
pub struct NumberLiteral<'a> {
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,14 @@
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use oxc_allocator::{Allocator, Box, String, Vec};
|
use oxc_allocator::{Allocator, Box, String, Vec};
|
||||||
use oxc_span::{Atom, SourceType, Span};
|
use oxc_span::{Atom, GetSpan, SourceType, Span};
|
||||||
use oxc_syntax::{
|
use oxc_syntax::{
|
||||||
operator::{
|
operator::{
|
||||||
AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator, UpdateOperator,
|
AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator, UpdateOperator,
|
||||||
},
|
},
|
||||||
NumberBase,
|
NumberBase,
|
||||||
};
|
};
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
#[allow(clippy::wildcard_imports)]
|
#[allow(clippy::wildcard_imports)]
|
||||||
use crate::ast::*;
|
use crate::ast::*;
|
||||||
|
|
@ -54,6 +55,13 @@ impl<'a> AstBuilder<'a> {
|
||||||
String::from_str_in(value, self.allocator).into_bump_str()
|
String::from_str_in(value, self.allocator).into_bump_str()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Moves the expression out by replacing it with a null expression.
|
||||||
|
pub fn move_expression(&self, expr: &mut Expression<'a>) -> Expression<'a> {
|
||||||
|
let null_literal = NullLiteral::new(expr.span());
|
||||||
|
let null_expr = self.literal_null_expression(null_literal);
|
||||||
|
mem::replace(expr, null_expr)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn program(
|
pub fn program(
|
||||||
&self,
|
&self,
|
||||||
span: Span,
|
span: Span,
|
||||||
|
|
|
||||||
23
crates/oxc_transformer/Cargo.toml
Normal file
23
crates/oxc_transformer/Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[package]
|
||||||
|
name = "oxc_transformer"
|
||||||
|
version = "0.2.0"
|
||||||
|
publish = false
|
||||||
|
authors.workspace = true
|
||||||
|
description.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
keywords.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
categories.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
oxc_ast = { workspace = true }
|
||||||
|
oxc_span = { workspace = true }
|
||||||
|
oxc_allocator = { workspace = true }
|
||||||
|
oxc_syntax = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
oxc_parser = { workspace = true }
|
||||||
|
oxc_formatter = { workspace = true }
|
||||||
41
crates/oxc_transformer/examples/transformer.rs
Normal file
41
crates/oxc_transformer/examples/transformer.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
use std::{env, path::Path};
|
||||||
|
|
||||||
|
use oxc_allocator::Allocator;
|
||||||
|
use oxc_formatter::{Formatter, FormatterOptions};
|
||||||
|
use oxc_parser::Parser;
|
||||||
|
use oxc_span::SourceType;
|
||||||
|
use oxc_transformer::{TransformOptions, Transformer};
|
||||||
|
|
||||||
|
// Instruction:
|
||||||
|
// create a `test.js`,
|
||||||
|
// run `cargo run -p oxc_transformer --example transformer`
|
||||||
|
// or `just watch "run -p oxc_transformer --example transformer"`
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let name = env::args().nth(1).unwrap_or_else(|| "test.js".to_string());
|
||||||
|
let path = Path::new(&name);
|
||||||
|
let source_text = std::fs::read_to_string(path).expect("{name} not found");
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let source_type = SourceType::from_path(path).unwrap();
|
||||||
|
let ret = Parser::new(&allocator, &source_text, source_type).parse();
|
||||||
|
|
||||||
|
if !ret.errors.is_empty() {
|
||||||
|
for error in ret.errors {
|
||||||
|
let error = error.with_source_code(source_text.clone());
|
||||||
|
println!("{error:?}");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let formatter_options = FormatterOptions::default();
|
||||||
|
let program = allocator.alloc(ret.program);
|
||||||
|
let printed = Formatter::new(source_text.len(), formatter_options.clone()).build(program);
|
||||||
|
println!("Original:\n");
|
||||||
|
println!("{printed}");
|
||||||
|
|
||||||
|
let transform_options = TransformOptions::default();
|
||||||
|
Transformer::new(&allocator, &transform_options).build(program);
|
||||||
|
let printed = Formatter::new(source_text.len(), formatter_options).build(program);
|
||||||
|
println!("Transformed:\n");
|
||||||
|
println!("{printed}");
|
||||||
|
}
|
||||||
42
crates/oxc_transformer/src/es2016/exponentiation_operator.rs
Normal file
42
crates/oxc_transformer/src/es2016/exponentiation_operator.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
use oxc_ast::{ast::*, AstBuilder};
|
||||||
|
use oxc_span::Span;
|
||||||
|
use oxc_syntax::operator::BinaryOperator;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// ES2016: Exponentiation Operator
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// * <https://babel.dev/docs/babel-plugin-transform-exponentiation-operator>
|
||||||
|
/// * <https://github.com/babel/babel/blob/main/packages/babel-plugin-transform-exponentiation-operator>
|
||||||
|
///
|
||||||
|
pub struct ExponentiationOperator<'a> {
|
||||||
|
ast: Rc<AstBuilder<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> ExponentiationOperator<'a> {
|
||||||
|
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
|
||||||
|
Self { ast }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform_expression<'b>(&mut self, expr: &'b mut Expression<'a>) {
|
||||||
|
let Expression::BinaryExpression(binary_expression) = expr else { return };
|
||||||
|
if binary_expression.operator != BinaryOperator::Exponential {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// left ** right
|
||||||
|
let left = self.ast.move_expression(&mut binary_expression.left);
|
||||||
|
let right = self.ast.move_expression(&mut binary_expression.right);
|
||||||
|
// Math.pow
|
||||||
|
let ident_math = IdentifierReference::new("Math".into(), Span::default());
|
||||||
|
let object = self.ast.identifier_expression(ident_math);
|
||||||
|
let property = IdentifierName::new(Span::default(), "pow".into());
|
||||||
|
let callee = self.ast.static_member_expression(Span::default(), object, property, false);
|
||||||
|
// Math.pow(left, right)
|
||||||
|
let mut arguments = self.ast.new_vec_with_capacity(2);
|
||||||
|
arguments.push(Argument::Expression(left));
|
||||||
|
arguments.push(Argument::Expression(right));
|
||||||
|
let call_expr = self.ast.call_expression(Span::default(), callee, arguments, false, None);
|
||||||
|
*expr = call_expr;
|
||||||
|
}
|
||||||
|
}
|
||||||
3
crates/oxc_transformer/src/es2016/mod.rs
Normal file
3
crates/oxc_transformer/src/es2016/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod exponentiation_operator;
|
||||||
|
|
||||||
|
pub use exponentiation_operator::ExponentiationOperator;
|
||||||
3
crates/oxc_transformer/src/es2019/mod.rs
Normal file
3
crates/oxc_transformer/src/es2019/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod optional_catch_binding;
|
||||||
|
|
||||||
|
pub use optional_catch_binding::OptionalCatchBinding;
|
||||||
29
crates/oxc_transformer/src/es2019/optional_catch_binding.rs
Normal file
29
crates/oxc_transformer/src/es2019/optional_catch_binding.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
use oxc_ast::{ast::*, AstBuilder};
|
||||||
|
use oxc_span::Span;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// ES2019: Optional Catch Binding
|
||||||
|
///
|
||||||
|
/// References:
|
||||||
|
/// * <https://babel.dev/docs/babel-plugin-transform-optional-catch-binding>
|
||||||
|
/// * <https://github.com/babel/babel/tree/main/packages/babel-plugin-transform-optional-catch-binding>
|
||||||
|
pub struct OptionalCatchBinding<'a> {
|
||||||
|
ast: Rc<AstBuilder<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> OptionalCatchBinding<'a> {
|
||||||
|
pub fn new(ast: Rc<AstBuilder<'a>>) -> Self {
|
||||||
|
Self { ast }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn transform_catch_clause<'b>(&mut self, clause: &'b mut CatchClause<'a>) {
|
||||||
|
if clause.param.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let binding_identifier = BindingIdentifier::new("unused".into(), Span::default());
|
||||||
|
let binding_pattern_kind = self.ast.binding_identifier(binding_identifier);
|
||||||
|
let binding_pattern = self.ast.binding_pattern(binding_pattern_kind, None, false);
|
||||||
|
clause.param = Some(binding_pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
76
crates/oxc_transformer/src/lib.rs
Normal file
76
crates/oxc_transformer/src/lib.rs
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
#![allow(clippy::wildcard_imports, clippy::option_map_unit_fn)]
|
||||||
|
|
||||||
|
//! Transformer / Transpiler
|
||||||
|
//!
|
||||||
|
//! References:
|
||||||
|
//! * <https://www.typescriptlang.org/tsconfig#target>
|
||||||
|
//! * <https://babel.dev/docs/presets>
|
||||||
|
//! * <https://github.com/microsoft/TypeScript/blob/main/src/compiler/transformer.ts>
|
||||||
|
|
||||||
|
mod es2016;
|
||||||
|
mod es2019;
|
||||||
|
|
||||||
|
use oxc_allocator::Allocator;
|
||||||
|
use oxc_ast::{ast::*, AstBuilder, VisitMut};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use es2016::ExponentiationOperator;
|
||||||
|
use es2019::OptionalCatchBinding;
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone)]
|
||||||
|
pub struct TransformOptions {
|
||||||
|
target: TransformTarget,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See <https://www.typescriptlang.org/tsconfig#target>
|
||||||
|
#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub enum TransformTarget {
|
||||||
|
ES2016,
|
||||||
|
ES2019,
|
||||||
|
#[default]
|
||||||
|
ESNext,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Transformer<'a> {
|
||||||
|
// es2016
|
||||||
|
es2016_exponentiation_operator: Option<ExponentiationOperator<'a>>,
|
||||||
|
// es2019
|
||||||
|
es2019_optional_catch_binding: Option<OptionalCatchBinding<'a>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Transformer<'a> {
|
||||||
|
pub fn new(allocator: &'a Allocator, options: &TransformOptions) -> Self {
|
||||||
|
let ast = Rc::new(AstBuilder::new(allocator));
|
||||||
|
|
||||||
|
let mut t = Self::default();
|
||||||
|
if options.target < TransformTarget::ES2016 {
|
||||||
|
t.es2016_exponentiation_operator.replace(ExponentiationOperator::new(Rc::clone(&ast)));
|
||||||
|
}
|
||||||
|
if options.target < TransformTarget::ES2019 {
|
||||||
|
t.es2019_optional_catch_binding.replace(OptionalCatchBinding::new(Rc::clone(&ast)));
|
||||||
|
}
|
||||||
|
t
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build<'b>(mut self, program: &'b mut Program<'a>) {
|
||||||
|
self.visit_program(program);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'b> VisitMut<'a, 'b> for Transformer<'a> {
|
||||||
|
fn visit_expression(&mut self, expr: &'b mut Expression<'a>) {
|
||||||
|
self.es2016_exponentiation_operator.as_mut().map(|t| t.transform_expression(expr));
|
||||||
|
|
||||||
|
self.visit_expression_match(expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_catch_clause(&mut self, clause: &'b mut CatchClause<'a>) {
|
||||||
|
self.es2019_optional_catch_binding.as_mut().map(|t| t.transform_catch_clause(clause));
|
||||||
|
|
||||||
|
if let Some(param) = &mut clause.param {
|
||||||
|
self.visit_binding_pattern(param);
|
||||||
|
}
|
||||||
|
self.visit_statements(&mut clause.body.body);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue