refactor(transformer/typescript): reimplementation of Enum conversion based on Babel (#3102)

The remaining test cases will perform better with a scope
implementation, and while we can implement them without the scope, it
still requires us to do what the scope did.

---------

Co-authored-by: Boshen <boshenc@gmail.com>
This commit is contained in:
Dunqing 2024-04-29 16:26:22 +08:00 committed by GitHub
parent 905ee3fef1
commit 843318cdbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 608 additions and 277 deletions

1
Cargo.lock generated
View file

@ -1655,6 +1655,7 @@ dependencies = [
"oxc_span",
"oxc_syntax",
"rustc-hash",
"ryu-js",
"serde",
]

View file

@ -29,6 +29,7 @@ oxc_syntax = { workspace = true }
rustc-hash = { workspace = true }
indexmap = { workspace = true }
serde = { workspace = true, features = ["derive"] }
ryu-js = { workspace = true }
[dev-dependencies]
oxc_parser = { workspace = true }

View file

@ -16,7 +16,6 @@ mod options;
mod es2015;
mod react;
mod typescript;
mod utils;
mod helpers {
pub mod module_imports;
@ -223,6 +222,7 @@ impl<'a> VisitMut<'a> for Transformer<'a> {
}
fn visit_statement(&mut self, stmt: &mut Statement<'a>) {
self.x0_typescript.transform_statement(stmt);
walk_mut::walk_statement_mut(self, stmt);
}

View file

@ -0,0 +1,83 @@
/// This file copied from [Boa](https://github.com/boa-dev/boa/blob/61567687cf4bfeca6bd548c3e72b6965e74b2461/core/engine/src/builtins/number/conversions.rs)
/// Converts a 64-bit floating point number to an `i32` according to the [`ToInt32`][ToInt32] algorithm.
///
/// [ToInt32]: https://tc39.es/ecma262/#sec-toint32
#[allow(clippy::float_cmp, clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
pub(crate) fn f64_to_int32(number: f64) -> i32 {
const SIGN_MASK: u64 = 0x8000_0000_0000_0000;
const EXPONENT_MASK: u64 = 0x7FF0_0000_0000_0000;
const SIGNIFICAND_MASK: u64 = 0x000F_FFFF_FFFF_FFFF;
const HIDDEN_BIT: u64 = 0x0010_0000_0000_0000;
const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit.
const SIGNIFICAND_SIZE: i32 = 53;
const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE;
const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1;
fn is_denormal(number: f64) -> bool {
(number.to_bits() & EXPONENT_MASK) == 0
}
fn exponent(number: f64) -> i32 {
if is_denormal(number) {
return DENORMAL_EXPONENT;
}
let d64 = number.to_bits();
let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32;
biased_e - EXPONENT_BIAS
}
fn significand(number: f64) -> u64 {
let d64 = number.to_bits();
let significand = d64 & SIGNIFICAND_MASK;
if is_denormal(number) {
significand
} else {
significand + HIDDEN_BIT
}
}
fn sign(number: f64) -> i64 {
if (number.to_bits() & SIGN_MASK) == 0 {
1
} else {
-1
}
}
if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) {
let i = number as i32;
if f64::from(i) == number {
return i;
}
}
let exponent = exponent(number);
let bits = if exponent < 0 {
if exponent <= -SIGNIFICAND_SIZE {
return 0;
}
significand(number) >> -exponent
} else {
if exponent > 31 {
return 0;
}
(significand(number) << exponent) & 0xFFFF_FFFF
};
(sign(number) * (bits as i64)) as i32
}
/// Converts a 64-bit floating point number to an `u32` according to the [`ToUint32`][ToUint32] algorithm.
///
/// [ToUint32]: https://tc39.es/ecma262/#sec-touint32
#[allow(clippy::cast_sign_loss)]
pub(crate) fn f64_to_uint32(number: f64) -> u32 {
f64_to_int32(number) as u32
}

View file

@ -1,33 +1,46 @@
use std::mem;
use std::rc::Rc;
use oxc_allocator::{Box, Vec};
use oxc_ast::ast::*;
use oxc_ast::{ast::*, visit::walk_mut, VisitMut};
use oxc_span::{Atom, SPAN};
use oxc_syntax::{
operator::{AssignmentOperator, BinaryOperator, LogicalOperator},
operator::{AssignmentOperator, BinaryOperator, LogicalOperator, UnaryOperator},
NumberBase,
};
use rustc_hash::FxHashMap;
use ryu_js::Buffer;
use crate::utils::is_valid_identifier;
use crate::context::Ctx;
use super::TypeScript;
use super::conversions::{f64_to_int32, f64_to_uint32};
impl<'a> TypeScript<'a> {
pub struct TypeScriptEnum<'a> {
ctx: Ctx<'a>,
enums: FxHashMap<Atom<'a>, FxHashMap<Atom<'a>, ConstantValue>>,
}
impl<'a> TypeScriptEnum<'a> {
pub fn new(ctx: &Ctx<'a>) -> Self {
Self { ctx: Rc::clone(ctx), enums: FxHashMap::default() }
}
/// ```TypeScript
/// enum Foo {
/// X
/// X = 1,
/// Y
/// }
/// ```
/// ```JavaScript
/// var Foo = ((Foo) => {
/// const X = 0; Foo[Foo["X"] = X] = "X";
/// Foo[Foo["X"] = 1] = "X";
/// Foo[Foo["Y"] = 2] = "Y";
/// return Foo;
/// })(Foo || {});
/// ```
pub fn transform_ts_enum(
&self,
decl: &mut Box<'a, TSEnumDeclaration<'a>>,
) -> Option<Declaration<'a>> {
&mut self,
decl: &Box<'a, TSEnumDeclaration<'a>>,
is_export: bool,
) -> Option<Statement<'a>> {
if decl.modifiers.contains(ModifierKind::Declare) {
return None;
}
@ -37,18 +50,10 @@ impl<'a> TypeScript<'a> {
let kind = self.ctx.ast.binding_pattern_identifier(ident);
let id = self.ctx.ast.binding_pattern(kind, None, false);
let mut params = self.ctx.ast.new_vec();
// ((Foo) => {
params.push(self.ctx.ast.formal_parameter(
SPAN,
id,
None,
false,
false,
self.ctx.ast.new_vec(),
));
let params =
self.ctx.ast.formal_parameter(SPAN, id, None, false, false, self.ctx.ast.new_vec());
let params = self.ctx.ast.new_vec_single(params);
let params = self.ctx.ast.formal_parameters(
SPAN,
FormalParameterKind::ArrowFormalParameters,
@ -58,26 +63,43 @@ impl<'a> TypeScript<'a> {
// Foo[Foo["X"] = 0] = "X";
let enum_name = decl.id.name.clone();
let statements = self.transform_ts_enum_members(&mut decl.members, &enum_name);
let is_already_declared = self.enums.contains_key(&enum_name);
let statements = self.transform_ts_enum_members(&decl.members, &enum_name);
let body = self.ctx.ast.function_body(decl.span, self.ctx.ast.new_vec(), statements);
let r#type = FunctionType::FunctionExpression;
let callee = self.ctx.ast.plain_function(r#type, SPAN, None, params, Some(body));
let callee = Expression::FunctionExpression(callee);
let callee =
self.ctx.ast.arrow_function_expression(SPAN, false, false, params, body, None, None);
// })(Foo || {});
let mut arguments = self.ctx.ast.new_vec();
let op = LogicalOperator::Or;
let left = self
.ctx
.ast
.identifier_reference_expression(IdentifierReference::new(SPAN, enum_name.clone()));
let right = self.ctx.ast.object_expression(SPAN, self.ctx.ast.new_vec(), None);
let expression = self.ctx.ast.logical_expression(SPAN, left, op, right);
arguments.push(Argument::from(expression));
let arguments = if is_export && !is_already_declared {
// }({});
let object_expr = self.ctx.ast.object_expression(SPAN, self.ctx.ast.new_vec(), None);
self.ctx.ast.new_vec_single(Argument::from(object_expr))
} else {
// }(Foo || {});
let op = LogicalOperator::Or;
let left = self
.ctx
.ast
.identifier_reference_expression(IdentifierReference::new(SPAN, enum_name.clone()));
let right = self.ctx.ast.object_expression(SPAN, self.ctx.ast.new_vec(), None);
let expression = self.ctx.ast.logical_expression(SPAN, left, op, right);
self.ctx.ast.new_vec_single(Argument::from(expression))
};
let call_expression = self.ctx.ast.call_expression(SPAN, callee, arguments, false, None);
let kind = VariableDeclarationKind::Var;
if is_already_declared {
let op = AssignmentOperator::Assign;
let left = self.ctx.ast.simple_assignment_target_identifier(IdentifierReference::new(
SPAN,
enum_name.clone(),
));
let expr = self.ctx.ast.assignment_expression(SPAN, op, left, call_expression);
return Some(self.ctx.ast.expression_statement(SPAN, expr));
}
let kind =
if is_export { VariableDeclarationKind::Let } else { VariableDeclarationKind::Var };
let decls = {
let mut decls = self.ctx.ast.new_vec();
@ -92,78 +114,106 @@ impl<'a> TypeScript<'a> {
};
let variable_declaration =
self.ctx.ast.variable_declaration(span, kind, decls, Modifiers::empty());
let variable_declaration = Declaration::VariableDeclaration(variable_declaration);
Some(Declaration::VariableDeclaration(variable_declaration))
let stmt = if is_export {
let declaration =
self.ctx.ast.plain_export_named_declaration_declaration(SPAN, variable_declaration);
self.ctx.ast.module_declaration(ModuleDeclaration::ExportNamedDeclaration(declaration))
} else {
Statement::from(variable_declaration)
};
Some(stmt)
}
pub fn transform_ts_enum_members(
&self,
members: &mut Vec<'a, TSEnumMember<'a>>,
&mut self,
members: &Vec<'a, TSEnumMember<'a>>,
enum_name: &Atom<'a>,
) -> Vec<'a, Statement<'a>> {
let mut default_init = self.ctx.ast.literal_number_expression(NumericLiteral {
span: SPAN,
value: 0.0,
raw: "0",
base: NumberBase::Decimal,
});
let mut statements = self.ctx.ast.new_vec();
let mut prev_constant_value = Some(ConstantValue::Number(-1.0));
let mut previous_enum_members = self.enums.entry(enum_name.clone()).or_default().clone();
let mut prev_member_name: Option<Atom<'a>> = None;
for member in members.iter_mut() {
let (member_name, member_span) = match &member.id {
TSEnumMemberName::StaticIdentifier(id) => (&id.name, id.span),
TSEnumMemberName::StaticStringLiteral(str) => (&str.value, str.span),
for member in members {
let member_name = match &member.id {
TSEnumMemberName::StaticIdentifier(id) => &id.name,
TSEnumMemberName::StaticStringLiteral(str) => &str.value,
#[allow(clippy::unnested_or_patterns)] // Clippy is wrong
TSEnumMemberName::StaticNumericLiteral(_) | match_expression!(TSEnumMemberName) => {
unreachable!()
}
};
let mut init = self
.ctx
.ast
.move_expression(member.initializer.as_mut().unwrap_or(&mut default_init));
let init = if let Some(initializer) = member.initializer.as_ref() {
let constant_value =
self.computed_constant_value(initializer, &previous_enum_members);
let is_str = init.is_string_literal();
// prev_constant_value = constant_value
let init = match constant_value {
None => {
prev_constant_value = None;
let mut new_initializer = self.ctx.ast.copy(initializer);
IdentifierReferenceRename::new(
enum_name.clone(),
previous_enum_members.clone(),
&self.ctx,
)
.visit_expression(&mut new_initializer);
new_initializer
}
Some(constant_value) => {
previous_enum_members.insert(member_name.clone(), constant_value.clone());
match constant_value {
ConstantValue::Number(v) => {
prev_constant_value = Some(ConstantValue::Number(v));
self.get_initializer_expr(v)
}
ConstantValue::String(str) => {
prev_constant_value = None;
self.ctx.ast.literal_string_expression(StringLiteral {
span: SPAN,
value: self.ctx.ast.new_atom(&str),
})
}
}
}
};
let mut self_ref = {
let obj = self.ctx.ast.identifier_reference_expression(IdentifierReference::new(
SPAN,
enum_name.clone(),
));
let expr = self
.ctx
.ast
.literal_string_expression(StringLiteral::new(SPAN, member_name.clone()));
self.ctx.ast.computed_member_expression(SPAN, obj, expr, false)
init
} else if let Some(ref value) = prev_constant_value {
match value {
ConstantValue::Number(value) => {
let value = value + 1.0;
let constant_value = ConstantValue::Number(value);
prev_constant_value = Some(constant_value.clone());
previous_enum_members.insert(member_name.clone(), constant_value);
self.get_initializer_expr(value)
}
ConstantValue::String(_) => unreachable!(),
}
} else if let Some(prev_member_name) = prev_member_name {
let self_ref = {
let obj = self.ctx.ast.identifier_reference_expression(
IdentifierReference::new(SPAN, enum_name.clone()),
);
let expr = self
.ctx
.ast
.literal_string_expression(StringLiteral::new(SPAN, prev_member_name));
self.ctx.ast.computed_member_expression(SPAN, obj, expr, false)
};
// 1 + Foo["x"]
let one = self.get_number_literal_expression(1.0);
self.ctx.ast.binary_expression(SPAN, one, BinaryOperator::Addition, self_ref)
} else {
self.get_number_literal_expression(0.0)
};
if is_valid_identifier(member_name, true) {
let ident = IdentifierReference::new(member_span, member_name.clone());
self_ref = self.ctx.ast.identifier_reference_expression(ident.clone());
let init =
mem::replace(&mut init, self.ctx.ast.identifier_reference_expression(ident));
let kind = VariableDeclarationKind::Const;
let decls = {
let mut decls = self.ctx.ast.new_vec();
let binding_identifier = BindingIdentifier::new(SPAN, member_name.clone());
let binding_pattern_kind =
self.ctx.ast.binding_pattern_identifier(binding_identifier);
let binding = self.ctx.ast.binding_pattern(binding_pattern_kind, None, false);
let decl =
self.ctx.ast.variable_declarator(SPAN, kind, binding, Some(init), false);
decls.push(decl);
decls
};
let decl = self.ctx.ast.variable_declaration(SPAN, kind, decls, Modifiers::empty());
let stmt: Statement<'_> = Statement::VariableDeclaration(decl);
statements.push(stmt);
}
let is_str = init.is_string_literal();
// Foo["x"] = init
let member_expr = {
@ -171,10 +221,8 @@ impl<'a> TypeScript<'a> {
SPAN,
enum_name.clone(),
));
let expr = self
.ctx
.ast
.literal_string_expression(StringLiteral::new(SPAN, member_name.clone()));
let literal = StringLiteral::new(SPAN, member_name.clone());
let expr = self.ctx.ast.literal_string_expression(literal);
self.ctx.ast.computed_member(SPAN, obj, expr, false)
};
@ -203,21 +251,12 @@ impl<'a> TypeScript<'a> {
);
}
prev_member_name = Some(member_name.clone());
statements.push(self.ctx.ast.expression_statement(member.span, expr));
// 1 + Foo["x"]
default_init = {
let one = self.ctx.ast.literal_number_expression(NumericLiteral {
span: SPAN,
value: 1.0,
raw: "1",
base: NumberBase::Decimal,
});
self.ctx.ast.binary_expression(SPAN, one, BinaryOperator::Addition, self_ref)
};
}
self.enums.insert(enum_name.clone(), previous_enum_members.clone());
let enum_ref = self
.ctx
.ast
@ -228,4 +267,278 @@ impl<'a> TypeScript<'a> {
statements
}
fn get_number_literal_expression(&self, value: f64) -> Expression<'a> {
self.ctx.ast.literal_number_expression(NumericLiteral {
span: SPAN,
value,
raw: self.ctx.ast.new_str(&value.to_string()),
base: NumberBase::Decimal,
})
}
fn get_initializer_expr(&self, value: f64) -> Expression<'a> {
let is_negative = value < 0.0;
// Infinity
let expr = if value.is_infinite() {
let ident = IdentifierReference::new(SPAN, self.ctx.ast.new_atom("Infinity"));
self.ctx.ast.identifier_reference_expression(ident)
} else {
let value = if is_negative { -value } else { value };
self.get_number_literal_expression(value)
};
if is_negative {
self.ctx.ast.unary_expression(SPAN, UnaryOperator::UnaryNegation, expr)
} else {
expr
}
}
}
#[derive(Debug, Clone)]
enum ConstantValue {
Number(f64),
String(String),
}
impl<'a> TypeScriptEnum<'a> {
/// Evaluate the expression to a constant value.
/// Refer to [babel](https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L241C1-L394C2)
fn computed_constant_value(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
) -> Option<ConstantValue> {
self.evaluate(expr, prev_members)
}
fn evalaute_ref(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
) -> Option<ConstantValue> {
match expr {
match_member_expression!(Expression) => {
let expr = expr.to_member_expression();
let Expression::Identifier(ident) = expr.object() else { return None };
let members = self.enums.get(&ident.name)?;
let property = expr.static_property_name()?;
return members.get(property).cloned();
}
Expression::Identifier(ident) => {
if ident.name == "Infinity" {
return Some(ConstantValue::Number(f64::INFINITY));
} else if ident.name == "NaN" {
return Some(ConstantValue::Number(f64::NAN));
}
if let Some(value) = prev_members.get(&ident.name) {
return Some(value.clone());
}
// TODO:
// This is a bit tricky because we need to find the BindingIdentifier that corresponds to the identifier reference.
// and then we may to evaluate the initializer of the BindingIdentifier.
// finally, we can get the value of the identifier and call the `computed_constant_value` function.
// See https://github.com/babel/babel/blob/610897a9a96c5e344e77ca9665df7613d2f88358/packages/babel-plugin-transform-typescript/src/enum.ts#L327-L329
None
}
_ => None,
}
}
fn evaluate(
&self,
expr: &Expression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
) -> Option<ConstantValue> {
match expr {
Expression::Identifier(_)
| Expression::ComputedMemberExpression(_)
| Expression::StaticMemberExpression(_)
| Expression::PrivateFieldExpression(_) => self.evalaute_ref(expr, prev_members),
Expression::BinaryExpression(expr) => self.eval_binary_expression(expr, prev_members),
Expression::UnaryExpression(expr) => self.eval_unary_expression(expr, prev_members),
Expression::NumericLiteral(lit) => Some(ConstantValue::Number(lit.value)),
Expression::StringLiteral(lit) => Some(ConstantValue::String(lit.value.to_string())),
Expression::TemplateLiteral(lit) => {
let mut value = String::new();
for part in &lit.quasis {
value.push_str(&part.value.raw);
}
Some(ConstantValue::String(value))
}
Expression::ParenthesizedExpression(expr) => {
self.evaluate(&expr.expression, prev_members)
}
_ => None,
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn eval_binary_expression(
&self,
expr: &BinaryExpression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
) -> Option<ConstantValue> {
let left = self.evaluate(&expr.left, prev_members)?;
let right = self.evaluate(&expr.right, prev_members)?;
if matches!(expr.operator, BinaryOperator::Addition)
&& (matches!(left, ConstantValue::String(_))
|| matches!(right, ConstantValue::String(_)))
{
let left_string = match left {
ConstantValue::String(str) => str,
ConstantValue::Number(v) => f64_to_js_string(v),
};
let right_string = match right {
ConstantValue::String(str) => str,
ConstantValue::Number(v) => f64_to_js_string(v),
};
return Some(ConstantValue::String(format!("{left_string}{right_string}")));
}
let left = match left {
ConstantValue::Number(v) => v,
ConstantValue::String(_) => return None,
};
let right = match right {
ConstantValue::Number(v) => v,
ConstantValue::String(_) => return None,
};
match expr.operator {
BinaryOperator::ShiftRight => Some(ConstantValue::Number(f64::from(
f64_to_int32(left).wrapping_shr(f64_to_uint32(right)),
))),
BinaryOperator::ShiftRightZeroFill => Some(ConstantValue::Number(f64::from(
f64_to_uint32(left).wrapping_shr(f64_to_uint32(right)),
))),
BinaryOperator::ShiftLeft => Some(ConstantValue::Number(f64::from(
f64_to_int32(left).wrapping_shl(f64_to_uint32(right)),
))),
BinaryOperator::BitwiseXOR => {
Some(ConstantValue::Number(f64::from(f64_to_int32(left) ^ f64_to_int32(right))))
}
BinaryOperator::BitwiseOR => {
Some(ConstantValue::Number(f64::from(f64_to_int32(left) | f64_to_int32(right))))
}
BinaryOperator::BitwiseAnd => {
Some(ConstantValue::Number(f64::from(f64_to_int32(left) & f64_to_int32(right))))
}
BinaryOperator::Multiplication => Some(ConstantValue::Number(left * right)),
BinaryOperator::Division => Some(ConstantValue::Number(left / right)),
BinaryOperator::Addition => Some(ConstantValue::Number(left + right)),
BinaryOperator::Subtraction => Some(ConstantValue::Number(left - right)),
BinaryOperator::Remainder => Some(ConstantValue::Number(left % right)),
BinaryOperator::Exponential => Some(ConstantValue::Number(left.powf(right))),
_ => None,
}
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]
fn eval_unary_expression(
&self,
expr: &UnaryExpression<'a>,
prev_members: &FxHashMap<Atom<'a>, ConstantValue>,
) -> Option<ConstantValue> {
let value = self.evaluate(&expr.argument, prev_members)?;
let value = match value {
ConstantValue::Number(value) => value,
ConstantValue::String(_) => {
let value = if expr.operator == UnaryOperator::UnaryNegation {
ConstantValue::Number(f64::NAN)
} else if expr.operator == UnaryOperator::BitwiseNot {
ConstantValue::Number(-1.0)
} else {
value
};
return Some(value);
}
};
match expr.operator {
UnaryOperator::UnaryPlus => Some(ConstantValue::Number(value)),
UnaryOperator::UnaryNegation => Some(ConstantValue::Number(-value)),
UnaryOperator::BitwiseNot => {
Some(ConstantValue::Number(f64::from(!f64_to_int32(value))))
}
_ => None,
}
}
}
/// Rename the identifier references in the enum members to `enum_name.identifier`
/// ```ts
/// enum A {
/// a = 1,
/// b = a.toString(),
/// d = c,
/// }
/// ```
/// will be transformed to
/// ```ts
/// enum A {
/// a = 1,
/// b = A.a.toString(),
/// d = A.c,
/// }
/// ```
struct IdentifierReferenceRename<'a> {
enum_name: Atom<'a>,
ctx: Ctx<'a>,
previous_enum_members: FxHashMap<Atom<'a>, ConstantValue>,
}
impl IdentifierReferenceRename<'_> {
fn new<'a>(
enum_name: Atom<'a>,
previous_enum_members: FxHashMap<Atom<'a>, ConstantValue>,
ctx: &Ctx<'a>,
) -> IdentifierReferenceRename<'a> {
IdentifierReferenceRename { enum_name, ctx: Rc::clone(ctx), previous_enum_members }
}
}
impl<'a> VisitMut<'a> for IdentifierReferenceRename<'a> {
fn visit_expression(&mut self, expr: &mut Expression<'a>) {
let new_expr = match expr {
match_member_expression!(Expression) => {
// handle a.toString() -> A.a.toString()
let expr = expr.to_member_expression();
if let Expression::Identifier(ident) = expr.object() {
if !self.previous_enum_members.contains_key(&ident.name) {
return;
}
};
None
}
Expression::Identifier(ident) => {
// TODO: shadowed case, e.g. let ident = 1; ident; // ident is not an enum
// enum_name.identifier
let ident_reference = IdentifierReference::new(SPAN, self.enum_name.clone());
let object = self.ctx.ast.identifier_reference_expression(ident_reference);
let property = self.ctx.ast.identifier_name(SPAN, &ident.name);
Some(self.ctx.ast.static_member_expression(SPAN, object, property, false))
}
_ => None,
};
if let Some(new_expr) = new_expr {
*expr = new_expr;
} else {
walk_mut::walk_expression_mut(self, expr);
}
}
}
fn f64_to_js_string(value: f64) -> String {
let mut buffer = Buffer::new();
buffer.format(value).to_string()
}

View file

@ -1,5 +1,6 @@
mod annotations;
mod collector;
mod conversions;
mod diagnostics;
mod r#enum;
mod module;
@ -14,7 +15,10 @@ use oxc_ast::ast::*;
use crate::context::Ctx;
use self::{annotations::TypeScriptAnnotations, collector::TypeScriptReferenceCollector};
use self::{
annotations::TypeScriptAnnotations, collector::TypeScriptReferenceCollector,
r#enum::TypeScriptEnum,
};
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(default, rename_all = "camelCase")]
@ -51,6 +55,7 @@ pub struct TypeScript<'a> {
ctx: Ctx<'a>,
annotations: TypeScriptAnnotations<'a>,
r#enum: TypeScriptEnum<'a>,
reference_collector: TypeScriptReferenceCollector<'a>,
}
@ -60,6 +65,7 @@ impl<'a> TypeScript<'a> {
Self {
annotations: TypeScriptAnnotations::new(&options, ctx),
r#enum: TypeScriptEnum::new(ctx),
reference_collector: TypeScriptReferenceCollector::new(),
options,
ctx: Rc::clone(ctx),
@ -141,6 +147,36 @@ impl<'a> TypeScript<'a> {
self.annotations.transform_statements_on_exit(stmts);
}
pub fn transform_statement(&mut self, stmt: &mut Statement<'a>) {
let new_stmt = match stmt {
match_declaration!(Statement) => {
if let Declaration::TSEnumDeclaration(ts_enum_decl) = &stmt.to_declaration() {
self.r#enum.transform_ts_enum(ts_enum_decl, false)
} else {
None
}
}
match_module_declaration!(Statement) => {
if let ModuleDeclaration::ExportNamedDeclaration(decl) =
stmt.to_module_declaration_mut()
{
if let Some(Declaration::TSEnumDeclaration(ts_enum_decl)) = &decl.declaration {
self.r#enum.transform_ts_enum(ts_enum_decl, true)
} else {
None
}
} else {
None
}
}
_ => None,
};
if let Some(new_stmt) = new_stmt {
*stmt = new_stmt;
}
}
pub fn transform_if_statement(&mut self, stmt: &mut IfStatement<'a>) {
self.annotations.transform_if_statement(stmt);
}
@ -163,11 +199,6 @@ impl<'a> TypeScript<'a> {
{
*decl = self.transform_ts_import_equals(ts_import_equals);
}
Declaration::TSEnumDeclaration(ts_enum_declaration) => {
if let Some(expr) = self.transform_ts_enum(ts_enum_declaration) {
*decl = expr;
}
}
_ => {}
}
}

View file

@ -1,12 +0,0 @@
use oxc_syntax::{identifier::is_identifier_name, keyword::is_keyword};
pub fn is_valid_identifier(name: &str, reserved: bool) -> bool {
if reserved && (is_keyword(name) || is_reserved_word(name, true)) {
return false;
}
is_identifier_name(name)
}
pub fn is_reserved_word(name: &str, in_module: bool) -> bool {
(in_module && name == "await") || name == "enum"
}

View file

@ -1,4 +1,4 @@
Passed: 274/347
Passed: 291/364
# All Passed:
* babel-plugin-transform-react-jsx-source
@ -23,19 +23,19 @@ Passed: 274/347
* opts/optimizeConstEnums/input.ts
* opts/rewriteImportExtensions/input.ts
# babel-plugin-transform-typescript (101/139)
# babel-plugin-transform-typescript (118/156)
* class/accessor-allowDeclareFields-false/input.ts
* class/accessor-allowDeclareFields-true/input.ts
* enum/mix-references/input.ts
* enum/scoped/input.ts
* enum/ts5.0-const-foldable/input.ts
* exports/declared-types/input.ts
* exports/export-const-enums/input.ts
* exports/export-type-star-from/input.ts
* imports/enum-id/input.ts
* imports/enum-value/input.ts
* imports/type-only-export-specifier-2/input.ts
* namespace/ambient-module-nested/input.ts
* namespace/ambient-module-nested-exported/input.ts
* namespace/canonical/input.ts
* namespace/clobber-enum/input.ts
* namespace/contentious-names/input.ts
* namespace/empty-removed/input.ts
* namespace/module-nested/input.ts

View file

@ -1,6 +1,7 @@
Passed: 1/1
Passed: 2/2
# All Passed:
* babel-plugin-transform-typescript
* babel-plugin-transform-react-jsx

View file

@ -120,8 +120,6 @@ pub(crate) const PLUGINS_NOT_SUPPORTED_YET: &[&str] = &[
"transform-react-constant-elements",
];
const EXCLUDE_TESTS: &[&str] = &["babel-plugin-transform-typescript/test/fixtures/enum"];
const CONFORMANCE_SNAPSHOT: &str = "babel.snap.md";
const OXC_CONFORMANCE_SNAPSHOT: &str = "oxc.snap.md";
const EXEC_SNAPSHOT: &str = "babel_exec.snap.md";
@ -186,9 +184,6 @@ impl TestRunner {
return None;
}
}
if EXCLUDE_TESTS.iter().any(|p| path.to_string_lossy().contains(p)) {
return None;
}
TestCaseKind::new(&cwd, path)
.filter(|test_case| !test_case.skip_test_case())
})

View file

@ -1,8 +1,5 @@
mod ts_fixtures;
use oxc_transform_conformance::{TestRunner, TestRunnerOptions};
use pico_args::Arguments;
use ts_fixtures::TypeScriptFixtures;
fn main() {
let mut args = Arguments::from_env();
@ -13,5 +10,4 @@ fn main() {
};
TestRunner::new(options.clone()).run();
TypeScriptFixtures::new(options).run();
}

View file

@ -1,136 +0,0 @@
use std::{
fs,
path::{Path, PathBuf},
};
use walkdir::WalkDir;
use oxc_allocator::Allocator;
use oxc_codegen::{Codegen, CodegenOptions};
use oxc_diagnostics::{miette::NamedSource, GraphicalReportHandler, GraphicalTheme};
use oxc_parser::Parser;
use oxc_semantic::SemanticBuilder;
use oxc_span::SourceType;
use oxc_tasks_common::{normalize_path, project_root};
use oxc_transform_conformance::TestRunnerOptions;
use oxc_transformer::{TransformOptions, Transformer};
fn root() -> PathBuf {
project_root().join("tasks/coverage")
}
fn snap_root() -> PathBuf {
project_root().join("tasks/transform_conformance")
}
const CASES: &[&str] = &[
"typescript/tests/cases/conformance/enums",
"babel/packages/babel-plugin-transform-typescript/test/fixtures/enum",
];
const CONFORMANCE_SNAPSHOT: &str = "typescript.snap.md";
fn filter_ext(p: &Path) -> bool {
p.to_string_lossy().ends_with(".ts")
}
pub struct TypeScriptFixtures {
options: TestRunnerOptions,
}
impl TypeScriptFixtures {
pub fn new(options: TestRunnerOptions) -> Self {
Self { options }
}
pub fn run(self) {
let mut snapshot = String::new();
for case in CASES {
for path in Self::glob_files(&root().join(case), self.options.filter.as_ref()) {
snapshot.push_str("# ");
snapshot.push_str(&normalize_path(path.strip_prefix(&root()).unwrap()));
snapshot.push('\n');
snapshot.push_str("```");
let (content, lang) = match Self::transform(&path) {
Ok(content) => (content, "typescript"),
Err(err) => (err, "error"),
};
snapshot.push_str(lang);
snapshot.push('\n');
snapshot.push_str(&content);
snapshot.push_str("\n```\n\n");
}
}
if self.options.filter.is_none() {
fs::write(snap_root().join(CONFORMANCE_SNAPSHOT), snapshot).unwrap();
}
}
}
impl TypeScriptFixtures {
fn transform_options() -> TransformOptions {
// TODO: read options from slash directives
TransformOptions::default()
}
fn glob_files(root: &Path, filter: Option<&String>) -> Vec<PathBuf> {
let mut list: Vec<PathBuf> = WalkDir::new(root)
.into_iter()
.filter_map(Result::ok)
.map(walkdir::DirEntry::into_path)
.filter(|p| p.is_file())
.filter(|p| filter_ext(p.as_path()))
.filter(|p| filter.map_or(true, |f| p.to_string_lossy().contains(f)))
.collect();
list.sort_unstable();
list
}
fn transform(path: &Path) -> Result<String, String> {
let allocator = Allocator::default();
let source_text = fs::read_to_string(path).unwrap();
let source_type = SourceType::from_path(path).unwrap();
let parser_ret = Parser::new(&allocator, &source_text, source_type).parse();
let semantic_ret = SemanticBuilder::new(&source_text, source_type)
.with_trivias(parser_ret.trivias)
.with_check_syntax_error(true)
.build_module_record(PathBuf::new(), &parser_ret.program)
.build(&parser_ret.program);
let errors = parser_ret.errors.into_iter().chain(semantic_ret.errors).collect::<Vec<_>>();
if !errors.is_empty() {
let handler =
GraphicalReportHandler::new().with_theme(GraphicalTheme::unicode_nocolor());
let mut output = String::new();
for error in errors {
let error = error.with_source_code(NamedSource::new(
&normalize_path(path.strip_prefix(&root()).unwrap()),
source_text.to_string(),
));
handler.render_report(&mut output, error.as_ref()).unwrap();
output.push('\n');
}
return Err(output);
}
let semantic = semantic_ret.semantic;
let transformed_program = allocator.alloc(parser_ret.program);
let result = Transformer::new(&allocator, path, semantic, Self::transform_options())
.build(transformed_program);
result
.map(|()| {
Codegen::<false>::new("", &source_text, CodegenOptions::default())
.build(transformed_program)
.source_text
})
.map_err(|e| e.iter().map(ToString::to_string).collect())
}
}

View file

@ -0,0 +1,27 @@
enum A {
a = Infinity,
b,
c = Infinity + 1,
d = Infinity + "test",
e = -(-(-Infinity)),
}
enum B {
a = NaN,
b,
c = NaN + 1,
d = "nan" + NaN,
e = -NaN,
}
enum C {
a = "test" + 1e20,
b = 1e30 + "test",
c = "test" + 1234567890987 + "test",
}
enum D {
a = +"hello",
b = -"hello",
c = ~"hello",
}

View file

@ -0,0 +1,28 @@
var A = /*#__PURE__*/function (A) {
A[A["a"] = Infinity] = "a";
A[A["b"] = Infinity] = "b";
A[A["c"] = Infinity] = "c";
A["d"] = "Infinitytest";
A[A["e"] = -Infinity] = "e";
return A;
}(A || {});
var B = /*#__PURE__*/function (B) {
B[B["a"] = NaN] = "a";
B[B["b"] = NaN] = "b";
B[B["c"] = NaN] = "c";
B["d"] = "nanNaN";
B[B["e"] = NaN] = "e";
return B;
}(B || {});
var C = /*#__PURE__*/function (C) {
C["a"] = "test100000000000000000000";
C["b"] = "1e+30test";
C["c"] = "test1234567890987test";
return C;
}(C || {});
var D = /*#__PURE__*/function (D) {
D["a"] = "hello";
D[D["b"] = NaN] = "b";
D[D["c"] = -1] = "c";
return D;
}(D || {});

View file

@ -0,0 +1,3 @@
{
"plugins": [["transform-typescript"]]
}