Skip to content

Commit

Permalink
feat(transformer): transform TypeScript namespace (#2942)
Browse files Browse the repository at this point in the history
  • Loading branch information
Boshen authored Apr 12, 2024
1 parent c250b28 commit 0c04bf7
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 13 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions crates/oxc_ast/src/ast_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions crates/oxc_transformer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }

Expand Down
1 change: 0 additions & 1 deletion crates/oxc_transformer/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}
Expand Down
10 changes: 7 additions & 3 deletions crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
}
Expand Down
9 changes: 7 additions & 2 deletions crates/oxc_transformer/src/typescript/mod.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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);
}
}
223 changes: 223 additions & 0 deletions crates/oxc_transformer/src/typescript/namespace.rs
Original file line number Diff line number Diff line change
@@ -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)
}
}
9 changes: 2 additions & 7 deletions tasks/transform_conformance/babel.snap.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 0c04bf7

Please sign in to comment.