From 843318cdbe58f6b45368cddcf1206b69eaef8639 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Mon, 29 Apr 2024 16:26:22 +0800 Subject: [PATCH] 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 --- Cargo.lock | 1 + crates/oxc_transformer/Cargo.toml | 1 + crates/oxc_transformer/src/lib.rs | 2 +- .../src/typescript/conversions.rs | 83 +++ crates/oxc_transformer/src/typescript/enum.rs | 527 ++++++++++++++---- crates/oxc_transformer/src/typescript/mod.rs | 43 +- crates/oxc_transformer/src/utils.rs | 12 - tasks/transform_conformance/babel.snap.md | 10 +- tasks/transform_conformance/oxc.snap.md | 3 +- tasks/transform_conformance/src/lib.rs | 5 - tasks/transform_conformance/src/main.rs | 4 - .../transform_conformance/src/ts_fixtures.rs | 136 ----- .../fixtures/computed-constant-value/input.ts | 27 + .../computed-constant-value/output.js | 28 + .../test/fixtures/options.json | 3 + 15 files changed, 608 insertions(+), 277 deletions(-) create mode 100644 crates/oxc_transformer/src/typescript/conversions.rs delete mode 100644 crates/oxc_transformer/src/utils.rs delete mode 100644 tasks/transform_conformance/src/ts_fixtures.rs create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/input.ts create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/output.js create mode 100644 tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/options.json diff --git a/Cargo.lock b/Cargo.lock index 8b6378079..016d2c2f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1655,6 +1655,7 @@ dependencies = [ "oxc_span", "oxc_syntax", "rustc-hash", + "ryu-js", "serde", ] diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index 20f460120..b34e6654a 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -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 } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index 9b89a20fe..dbd288208 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -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); } diff --git a/crates/oxc_transformer/src/typescript/conversions.rs b/crates/oxc_transformer/src/typescript/conversions.rs new file mode 100644 index 000000000..3113ead04 --- /dev/null +++ b/crates/oxc_transformer/src/typescript/conversions.rs @@ -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 +} diff --git a/crates/oxc_transformer/src/typescript/enum.rs b/crates/oxc_transformer/src/typescript/enum.rs index b44474510..ad649c657 100644 --- a/crates/oxc_transformer/src/typescript/enum.rs +++ b/crates/oxc_transformer/src/typescript/enum.rs @@ -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, FxHashMap, 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> { + &mut self, + decl: &Box<'a, TSEnumDeclaration<'a>>, + is_export: bool, + ) -> Option> { 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> = 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, ConstantValue>, + ) -> Option { + self.evaluate(expr, prev_members) + } + + fn evalaute_ref( + &self, + expr: &Expression<'a>, + prev_members: &FxHashMap, ConstantValue>, + ) -> Option { + 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, ConstantValue>, + ) -> Option { + 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, ConstantValue>, + ) -> Option { + 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, ConstantValue>, + ) -> Option { + 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, ConstantValue>, +} + +impl IdentifierReferenceRename<'_> { + fn new<'a>( + enum_name: Atom<'a>, + previous_enum_members: FxHashMap, 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() } diff --git a/crates/oxc_transformer/src/typescript/mod.rs b/crates/oxc_transformer/src/typescript/mod.rs index 8a6fb405f..9203451c7 100644 --- a/crates/oxc_transformer/src/typescript/mod.rs +++ b/crates/oxc_transformer/src/typescript/mod.rs @@ -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; - } - } _ => {} } } diff --git a/crates/oxc_transformer/src/utils.rs b/crates/oxc_transformer/src/utils.rs deleted file mode 100644 index 31a4190f5..000000000 --- a/crates/oxc_transformer/src/utils.rs +++ /dev/null @@ -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" -} diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index c4b9e9a9d..eb774ada1 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -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 diff --git a/tasks/transform_conformance/oxc.snap.md b/tasks/transform_conformance/oxc.snap.md index c31db523d..5d4b70a07 100644 --- a/tasks/transform_conformance/oxc.snap.md +++ b/tasks/transform_conformance/oxc.snap.md @@ -1,6 +1,7 @@ -Passed: 1/1 +Passed: 2/2 # All Passed: +* babel-plugin-transform-typescript * babel-plugin-transform-react-jsx diff --git a/tasks/transform_conformance/src/lib.rs b/tasks/transform_conformance/src/lib.rs index af93b9030..21a8ef538 100644 --- a/tasks/transform_conformance/src/lib.rs +++ b/tasks/transform_conformance/src/lib.rs @@ -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()) }) diff --git a/tasks/transform_conformance/src/main.rs b/tasks/transform_conformance/src/main.rs index d3d7504e7..92fe3bccf 100644 --- a/tasks/transform_conformance/src/main.rs +++ b/tasks/transform_conformance/src/main.rs @@ -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(); } diff --git a/tasks/transform_conformance/src/ts_fixtures.rs b/tasks/transform_conformance/src/ts_fixtures.rs deleted file mode 100644 index 9aee8e01b..000000000 --- a/tasks/transform_conformance/src/ts_fixtures.rs +++ /dev/null @@ -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 { - let mut list: Vec = 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 { - 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::>(); - - 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::::new("", &source_text, CodegenOptions::default()) - .build(transformed_program) - .source_text - }) - .map_err(|e| e.iter().map(ToString::to_string).collect()) - } -} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/input.ts b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/input.ts new file mode 100644 index 000000000..05a50ec84 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/input.ts @@ -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", +} diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/output.js b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/output.js new file mode 100644 index 000000000..58f370ac7 --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/computed-constant-value/output.js @@ -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 || {}); \ No newline at end of file diff --git a/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/options.json b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/options.json new file mode 100644 index 000000000..ae694e68b --- /dev/null +++ b/tasks/transform_conformance/tests/babel-plugin-transform-typescript/test/fixtures/options.json @@ -0,0 +1,3 @@ +{ + "plugins": [["transform-typescript"]] +}