diff --git a/Cargo.lock b/Cargo.lock index db1fcf123..edc632bfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1737,6 +1737,8 @@ dependencies = [ "oxc_parser", "oxc_semantic", "oxc_span", + "oxc_syntax", + "rustc-hash", "serde", ] diff --git a/crates/oxc_ast/src/ast_builder.rs b/crates/oxc_ast/src/ast_builder.rs index a1a6de961..e832fdc47 100644 --- a/crates/oxc_ast/src/ast_builder.rs +++ b/crates/oxc_ast/src/ast_builder.rs @@ -822,6 +822,14 @@ impl<'a> AstBuilder<'a> { self.alloc(FormalParameters { span, kind, items, rest }) } + pub fn plain_formal_parameter( + &self, + span: Span, + pattern: BindingPattern<'a>, + ) -> FormalParameter<'a> { + self.formal_parameter(span, pattern, None, false, false, self.new_vec()) + } + pub fn formal_parameter( &self, span: Span, @@ -843,6 +851,29 @@ impl<'a> AstBuilder<'a> { TSThisParameter { span, this, type_annotation } } + pub fn plain_function( + &self, + r#type: FunctionType, + span: Span, + id: Option>, + params: Box<'a, FormalParameters<'a>>, + body: Option>>, + ) -> Box<'a, Function<'a>> { + self.function( + r#type, + span, + id, + false, + false, + None, + params, + body, + None, + None, + Modifiers::empty(), + ) + } + pub fn function( &self, r#type: FunctionType, diff --git a/crates/oxc_transformer/Cargo.toml b/crates/oxc_transformer/Cargo.toml index 3200278ba..b9ac5ed71 100644 --- a/crates/oxc_transformer/Cargo.toml +++ b/crates/oxc_transformer/Cargo.toml @@ -24,6 +24,9 @@ oxc_span = { workspace = true } oxc_allocator = { workspace = true } oxc_semantic = { workspace = true } oxc_diagnostics = { workspace = true } +oxc_syntax = { workspace = true } + +rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } diff --git a/crates/oxc_transformer/src/context.rs b/crates/oxc_transformer/src/context.rs index 1dce7a44d..9b0596f4a 100644 --- a/crates/oxc_transformer/src/context.rs +++ b/crates/oxc_transformer/src/context.rs @@ -37,7 +37,6 @@ impl<'a> TransformCtx<'a> { } /// Add an Error - #[allow(unused)] pub fn error>(&self, error: T) { self.errors.borrow_mut().push(error.into()); } diff --git a/crates/oxc_transformer/src/lib.rs b/crates/oxc_transformer/src/lib.rs index eb34b2864..2029c6ec4 100644 --- a/crates/oxc_transformer/src/lib.rs +++ b/crates/oxc_transformer/src/lib.rs @@ -17,7 +17,7 @@ mod typescript; use std::{path::Path, rc::Rc}; -use oxc_allocator::Allocator; +use oxc_allocator::{Allocator, Vec}; use oxc_ast::{ ast::*, visit::{walk_mut, VisitMut}, @@ -81,7 +81,7 @@ impl<'a> Transformer<'a> { /// # Errors /// /// Returns `Vec` if any errors were collected during the transformation. - pub fn build(mut self, program: &mut Program<'a>) -> Result<(), Vec> { + pub fn build(mut self, program: &mut Program<'a>) -> Result<(), std::vec::Vec> { self.visit_program(program); let errors = self.ctx.take_errors(); if errors.is_empty() { @@ -93,8 +93,12 @@ impl<'a> Transformer<'a> { } impl<'a> VisitMut<'a> for Transformer<'a> { + fn visit_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + self.x0_typescript.transform_statements(stmts); + walk_mut::walk_statements_mut(self, stmts); + } + fn visit_statement(&mut self, stmt: &mut Statement<'a>) { - self.x0_typescript.transform_statement(stmt); self.x2_decorators.transform_statement(stmt); walk_mut::walk_statement_mut(self, stmt); } diff --git a/crates/oxc_transformer/src/typescript/mod.rs b/crates/oxc_transformer/src/typescript/mod.rs index 522061519..ca491c8ab 100644 --- a/crates/oxc_transformer/src/typescript/mod.rs +++ b/crates/oxc_transformer/src/typescript/mod.rs @@ -1,7 +1,10 @@ +mod namespace; + use std::rc::Rc; use serde::Deserialize; +use oxc_allocator::Vec; use oxc_ast::ast::*; use crate::context::Ctx; @@ -43,7 +46,9 @@ impl<'a> TypeScript<'a> { } } -// Transformers +// Transforms impl<'a> TypeScript<'a> { - pub fn transform_statement(&mut self, _stmt: &mut Statement<'a>) {} + pub fn transform_statements(&mut self, stmts: &mut Vec<'a, Statement<'a>>) { + self.transform_statements_for_namespace(stmts); + } } diff --git a/crates/oxc_transformer/src/typescript/namespace.rs b/crates/oxc_transformer/src/typescript/namespace.rs new file mode 100644 index 000000000..7fab8e24e --- /dev/null +++ b/crates/oxc_transformer/src/typescript/namespace.rs @@ -0,0 +1,223 @@ +use rustc_hash::{FxHashMap, FxHashSet}; + +use super::TypeScript; + +use oxc_allocator::{Box, Vec}; +use oxc_ast::ast::*; +use oxc_span::{Atom, SPAN}; +use oxc_syntax::operator::{AssignmentOperator, LogicalOperator}; + +#[derive(Default)] +struct State<'a> { + /// Deduplicate the `let` declarations` for namespace concatenation. + /// `namespace foo {}; namespace {}` creates a single `let foo;`. + names: FxHashSet>, + + /// Increment the argument name to avoid name clashes. + arg_names: FxHashMap, usize>, +} + +fn is_namespace(decl: &Declaration<'_>) -> bool { + matches!(decl, Declaration::TSModuleDeclaration(decl) if !decl.modifiers.is_contains_declare()) +} + +// TODO: +// 1. register scope for the newly created function: +impl<'a> TypeScript<'a> { + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + pub(super) fn transform_statements_for_namespace(&self, stmts: &mut Vec<'a, Statement<'a>>) { + // Only do the transform if a namespace declaration is found. + if !stmts.iter().any(|stmt| match stmt { + Statement::Declaration(decl) => is_namespace(decl), + Statement::ModuleDeclaration(decl) => match &**decl { + ModuleDeclaration::ExportNamedDeclaration(decl) => { + decl.declaration.as_ref().is_some_and(is_namespace) + } + _ => false, + }, + _ => false, + }) { + return; + } + + // Recreate the statements vec for memory efficiency. + // Inserting the `let` declaration multiple times will reallocate the whole statements vec + // every time a namespace declaration is encountered. + let mut new_stmts = self.ctx.ast.new_vec(); + + let mut state = State::default(); + + for mut stmt in self.ctx.ast.move_statement_vec(stmts) { + if !self.transform_statement_for_namespace(&mut state, &mut new_stmts, &mut stmt) { + new_stmts.push(stmt); + } + } + + *stmts = new_stmts; + } + + fn transform_statement_for_namespace( + &self, + state: &mut State<'a>, + new_stmts: &mut Vec<'a, Statement<'a>>, + stmt: &mut Statement<'a>, + ) -> bool { + let mut is_export = false; + let ts_module_decl = match stmt { + Statement::Declaration(Declaration::TSModuleDeclaration(ts_module_decl)) => { + ts_module_decl + } + Statement::ModuleDeclaration(decl) => match &mut **decl { + ModuleDeclaration::ExportNamedDeclaration(decl) => { + if let Some(Declaration::TSModuleDeclaration(ts_module_decl)) = + decl.declaration.as_mut() + { + is_export = true; + ts_module_decl + } else { + return false; + } + } + _ => return false, + }, + _ => return false, + }; + + if ts_module_decl.modifiers.is_contains_declare() { + return false; + } + + let name = ts_module_decl.id.name().clone(); + + if state.names.insert(name.clone()) { + let stmt = self.create_variable_declaration_statement(&name, is_export); + new_stmts.push(stmt); + } + + let namespace = self.transform_namespace(state, ts_module_decl); + new_stmts.push(namespace); + true + } + + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + // ^^^^^^^ + fn create_variable_declaration_statement( + &self, + name: &Atom<'a>, + is_export: bool, + ) -> Statement<'a> { + let kind = VariableDeclarationKind::Let; + let declarators = { + let ident = BindingIdentifier::new(SPAN, name.clone()); + let pattern_kind = self.ctx.ast.binding_pattern_identifier(ident); + let binding = self.ctx.ast.binding_pattern(pattern_kind, None, false); + let decl = self.ctx.ast.variable_declarator(SPAN, kind, binding, None, false); + self.ctx.ast.new_vec_single(decl) + }; + let decl = Declaration::VariableDeclaration(self.ctx.ast.variable_declaration( + SPAN, + kind, + declarators, + Modifiers::empty(), + )); + if is_export { + self.ctx.ast.module_declaration(ModuleDeclaration::ExportNamedDeclaration( + self.ctx.ast.export_named_declaration( + SPAN, + Some(decl), + self.ctx.ast.new_vec(), + None, + ImportOrExportKind::Value, + None, + ), + )) + } else { + Statement::Declaration(decl) + } + } + + // `namespace Foo { }` -> `let Foo; (function (_Foo) { })(Foo || (Foo = {}));` + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + fn transform_namespace( + &self, + state: &mut State<'a>, + block: &mut Box<'a, TSModuleDeclaration<'a>>, + ) -> Statement<'a> { + let body_statements = match &mut block.body { + Some(TSModuleDeclarationBody::TSModuleDeclaration(decl)) => { + let transformed_module_block = self.transform_namespace(state, decl); + self.ctx.ast.new_vec_single(transformed_module_block) + } + Some(TSModuleDeclarationBody::TSModuleBlock(ts_module_block)) => { + self.ctx.ast.move_statement_vec(&mut ts_module_block.body) + } + None => self.ctx.ast.new_vec(), + }; + + let name = block.id.name(); + + // `(function (_N) { var x; })(N || (N = {}))`; + // ^^^^^^^^^^^^^^^^^^^^^^^^^^ + let callee = { + let body = self.ctx.ast.function_body(SPAN, self.ctx.ast.new_vec(), body_statements); + let arg_name = self.get_namespace_arg_name(state, name); + let params = { + let ident = + self.ctx.ast.binding_pattern_identifier(BindingIdentifier::new(SPAN, arg_name)); + let pattern = self.ctx.ast.binding_pattern(ident, None, false); + let items = + self.ctx.ast.new_vec_single(self.ctx.ast.plain_formal_parameter(SPAN, pattern)); + self.ctx.ast.formal_parameters( + SPAN, + FormalParameterKind::FormalParameter, + items, + None, + ) + }; + let function = self.ctx.ast.plain_function( + FunctionType::FunctionExpression, + SPAN, + None, + params, + Some(body), + ); + let function_expr = self.ctx.ast.function_expression(function); + self.ctx.ast.parenthesized_expression(SPAN, function_expr) + }; + + // `(function (_N) { var x; })(N || (N = {}))`; + // ^^^^^^^^^^^^^ + let arguments = { + let logical_left = { + let ident = IdentifierReference::new(SPAN, name.clone()); + self.ctx.ast.identifier_reference_expression(ident) + }; + let logical_right = { + let assign_left = self.ctx.ast.simple_assignment_target_identifier( + IdentifierReference::new(SPAN, name.clone()), + ); + let assign_right = + self.ctx.ast.object_expression(SPAN, self.ctx.ast.new_vec(), None); + let op = AssignmentOperator::Assign; + let assign_expr = + self.ctx.ast.assignment_expression(SPAN, op, assign_left, assign_right); + self.ctx.ast.parenthesized_expression(SPAN, assign_expr) + }; + self.ctx.ast.new_vec_single(Argument::Expression(self.ctx.ast.logical_expression( + SPAN, + logical_left, + LogicalOperator::Or, + logical_right, + ))) + }; + let expr = self.ctx.ast.call_expression(SPAN, callee, arguments, false, None); + self.ctx.ast.expression_statement(SPAN, expr) + } + + fn get_namespace_arg_name(&self, state: &mut State<'a>, name: &Atom<'a>) -> Atom<'a> { + let count = state.arg_names.entry(name.clone()).or_insert(0); + *count += 1; + let name = if *count > 1 { format!("_{name}{count}") } else { format!("_{name}") }; + self.ctx.ast.new_atom(&name) + } +} diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 60507c1cd..4a2f73753 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,10 +1,10 @@ -Passed: 70/174 +Passed: 75/174 # All Passed: -# babel-plugin-transform-typescript (55/158) +# babel-plugin-transform-typescript (60/158) * class/abstract-class-decorated/input.ts * class/abstract-class-decorated-method/input.ts * class/abstract-class-decorated-parameter/input.ts @@ -74,14 +74,10 @@ Passed: 70/174 * namespace/clobber-export/input.ts * namespace/clobber-import/input.ts * namespace/contentious-names/input.ts -* namespace/declare/input.ts -* namespace/declare-global-nested-namespace/input.ts * namespace/empty-removed/input.ts -* namespace/export/input.ts * namespace/export-type-only/input.ts * namespace/module-nested/input.ts * namespace/module-nested-export/input.ts -* namespace/multiple/input.ts * namespace/mutable-fail/input.ts * namespace/namespace-flag/input.ts * namespace/namespace-nested-module/input.ts @@ -91,7 +87,6 @@ Passed: 70/174 * namespace/nested-shorthand/input.ts * namespace/nested-shorthand-export/input.ts * namespace/same-name/input.ts -* namespace/undeclared/input.ts * optimize-const-enums/custom-values/input.ts * optimize-const-enums/custom-values-exported/input.ts * optimize-const-enums/declare/input.ts diff --git a/tasks/transform_conformance/typescript.snap.md b/tasks/transform_conformance/typescript.snap.md index 5374955d0..65fa5da49 100644 --- a/tasks/transform_conformance/typescript.snap.md +++ b/tasks/transform_conformance/typescript.snap.md @@ -83,11 +83,61 @@ const {value='123'} = thing; # typescript/tests/cases/conformance/enums/enumMerging.ts ```typescript +let M1; +(function(_M1) { + var x = [EConst1.A, EConst1.B, EConst1.C, EConst1.D, EConst1.E, EConst1.F]; +})(M1 || (M1 = {})); +let M2; +(function(_M2) { + var x = [EComp2.A, EComp2.B, EComp2.C, EComp2.D, EComp2.E, EComp2.F]; +})(M2 || (M2 = {})); +let M3; +(function(_M3) { +})(M3 || (M3 = {})); +let M4; +(function(_M4) { +})(M4 || (M4 = {})); +let M5; +(function(_M5) { +})(M5 || (M5 = {})); +let M6; +(function(_M6) { + (function(_A) { + })(A || (A = {})); +})(M6 || (M6 = {})); +(function(_M62) { +export let A; + (function(_A) { + })(A || (A = {})); + var t = A.Color.Yellow; + t = A.Color.Red; +})(M6 || (M6 = {})); ``` # typescript/tests/cases/conformance/enums/enumMergingErrors.ts ```typescript +let M; +(function(_M) { +})(M || (M = {})); +(function(_M2) { +})(M || (M = {})); +(function(_M3) { +})(M || (M = {})); +let M1; +(function(_M1) { +})(M1 || (M1 = {})); +(function(_M12) { +})(M1 || (M1 = {})); +(function(_M13) { +})(M1 || (M1 = {})); +let M2; +(function(_M2) { +})(M2 || (M2 = {})); +(function(_M22) { +})(M2 || (M2 = {})); +(function(_M23) { +})(M2 || (M2 = {})); ```