feat(transformer): transform TypeScript namespace (#2942)

This commit is contained in:
Boshen 2024-04-12 10:19:13 +08:00 committed by GitHub
parent c250b288ef
commit 0c04bf743f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 325 additions and 13 deletions

2
Cargo.lock generated
View file

@ -1737,6 +1737,8 @@ dependencies = [
"oxc_parser",
"oxc_semantic",
"oxc_span",
"oxc_syntax",
"rustc-hash",
"serde",
]

View file

@ -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<BindingIdentifier<'a>>,
params: Box<'a, FormalParameters<'a>>,
body: Option<Box<'a, FunctionBody<'a>>>,
) -> 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,

View file

@ -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"] }

View file

@ -37,7 +37,6 @@ impl<'a> TransformCtx<'a> {
}
/// Add an Error
#[allow(unused)]
pub fn error<T: Into<Error>>(&self, error: T) {
self.errors.borrow_mut().push(error.into());
}

View file

@ -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<Error>` if any errors were collected during the transformation.
pub fn build(mut self, program: &mut Program<'a>) -> Result<(), Vec<Error>> {
pub fn build(mut self, program: &mut Program<'a>) -> Result<(), std::vec::Vec<Error>> {
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);
}

View file

@ -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);
}
}

View file

@ -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<Atom<'a>>,
/// Increment the argument name to avoid name clashes.
arg_names: FxHashMap<Atom<'a>, 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: <https://github.com/babel/babel/blob/08b0472069cd207f043dd40a4d157addfdd36011/packages/babel-plugin-transform-typescript/src/namespace.ts#L38>
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)
}
}

View file

@ -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

View file

@ -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 = {}));
```