Skip to content

Commit

Permalink
feat(transformer/typescript): correct elide imports/exports statements
Browse files Browse the repository at this point in the history
  • Loading branch information
Dunqing committed Apr 15, 2024
1 parent 82e00bc commit df6d20c
Show file tree
Hide file tree
Showing 5 changed files with 176 additions and 110 deletions.
12 changes: 10 additions & 2 deletions crates/oxc_transformer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,8 +143,6 @@ impl<'a> VisitMut<'a> for Transformer<'a> {
}

fn visit_import_declaration(&mut self, decl: &mut ImportDeclaration<'a>) {
self.x0_typescript.transform_import_declaration(decl);

walk_mut::walk_import_declaration_mut(self, decl);
}

Expand Down Expand Up @@ -198,4 +196,14 @@ impl<'a> VisitMut<'a> for Transformer<'a> {

walk_mut::walk_variable_declarator_mut(self, declarator);
}

fn visit_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) {
self.x0_typescript.transform_identifier_reference(ident);
walk_mut::walk_identifier_reference_mut(self, ident);
}

fn visit_statement(&mut self, stmt: &mut Statement<'a>) {
self.x0_typescript.transform_statement(stmt);
walk_mut::walk_statement_mut(self, stmt);
}
}
177 changes: 109 additions & 68 deletions crates/oxc_transformer/src/typescript/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,25 +11,17 @@ use oxc_span::{Atom, SPAN};
use oxc_syntax::operator::AssignmentOperator;
use rustc_hash::FxHashSet;

use super::collector::TypeScriptReferenceCollector;

pub struct TypeScriptAnnotations<'a> {
#[allow(dead_code)]
options: Rc<TypeScriptOptions>,
ctx: Ctx<'a>,

global_types: FxHashSet<String>,
}

impl<'a> TypeScriptAnnotations<'a> {
pub fn new(options: &Rc<TypeScriptOptions>, ctx: &Ctx<'a>) -> Self {
Self {
options: Rc::clone(options),
ctx: Rc::clone(ctx),
global_types: FxHashSet::default(),
}
}

pub fn is_global_type(&self) -> bool {
self.global_types.contains("TODO")
Self { options: Rc::clone(options), ctx: Rc::clone(ctx) }
}

// Convert `export = expr` into `module.exports = expr`
Expand Down Expand Up @@ -73,57 +65,113 @@ impl<'a> TypeScriptAnnotations<'a> {
}

// Remove type only imports/exports
pub fn transform_program_on_exit(&self, program: &mut Program<'a>) {
pub fn transform_program_on_exit(
&self,
program: &mut Program<'a>,
references: &TypeScriptReferenceCollector,
) {
let mut import_type_names = FxHashSet::default();
let mut module_count = 0;

let body =
self.ctx.ast.move_statement_vec(&mut program.body).into_iter().filter_map(|stmt| {
// If an import/export declaration, remove all that are type-only
if let Statement::ModuleDeclaration(decl) = &stmt {
let keep = match &**decl {
ModuleDeclaration::ImportDeclaration(inner) => !inner.import_kind.is_type(),
ModuleDeclaration::ExportAllDeclaration(inner) => {
!inner.is_typescript_syntax()
}
ModuleDeclaration::ExportNamedDeclaration(inner) => {
!(inner.is_typescript_syntax()
|| inner.specifiers.is_empty()
|| inner.specifiers.iter().all(|spec| spec.export_kind.is_type())
|| self.is_global_type())
}
ModuleDeclaration::ExportDefaultDeclaration(inner) => {
!inner.is_typescript_syntax()
}
ModuleDeclaration::TSNamespaceExportDeclaration(_) => false,

// Replace with `module.exports = expr`
ModuleDeclaration::TSExportAssignment(exp) => {
return Some(self.create_module_exports(exp));
}
};

if keep {
module_count += 1;
} else {
return None;
let mut removed_count = 0;

program.body.retain_mut(|stmt| {
let Statement::ModuleDeclaration(module_decl) = stmt else {
return true;
};

let need_delete = match &mut **module_decl {
ModuleDeclaration::ExportNamedDeclaration(decl) => {
decl.specifiers.retain(|specifier| {
!(specifier.export_kind.is_type()
|| import_type_names.contains(specifier.exported.name()))
});

decl.export_kind.is_type()
|| ((decl.declaration.is_none()
|| decl.declaration.as_ref().is_some_and(|d| {
d.modifiers().is_some_and(|modifiers| {
modifiers.contains(ModifierKind::Declare)
}) || matches!(
d,
Declaration::TSInterfaceDeclaration(_)
| Declaration::TSTypeAliasDeclaration(_)
)
}))
&& decl.specifiers.is_empty())
}
ModuleDeclaration::ImportDeclaration(decl) => {
let is_type = decl.import_kind.is_type();

let is_specifiers_empty =
decl.specifiers.as_ref().is_some_and(|s| s.is_empty());

if let Some(specifiers) = &mut decl.specifiers {
specifiers.retain(|specifier| match specifier {
ImportDeclarationSpecifier::ImportSpecifier(s) => {
if is_type || s.import_kind.is_type() {
import_type_names.insert(s.local.name.clone());
return false;
}

if self.options.only_remove_type_imports {
return true;
}

references.has_reference(&s.local.name)
}
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
if is_type {
import_type_names.insert(s.local.name.clone());
return false;
}

if self.options.only_remove_type_imports {
return true;
}
references.has_reference(&s.local.name)
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
if is_type {
import_type_names.insert(s.local.name.clone());
}

if self.options.only_remove_type_imports {
return true;
}

references.has_reference(&s.local.name)
}
});
}

decl.import_kind.is_type()
|| (!self.options.only_remove_type_imports
&& !is_specifiers_empty
&& decl
.specifiers
.as_ref()
.is_some_and(|specifiers| specifiers.is_empty()))
}
_ => false,
};

Some(stmt)
});
if need_delete {
removed_count += 1;
} else {
module_count += 1;
}

program.body = self.ctx.ast.new_vec_from_iter(body);
!need_delete
});

// Determine if we still have import/export statements, otherwise we
// need to inject an empty statement (`export {}`) so that the file is
// still considered a module
if module_count == 0 && self.ctx.semantic.source_type().is_module() {
// FIXME
// program.body.push(self.ctx.ast.module_declaration(
// ModuleDeclaration::ExportNamedDeclaration(
// self.ctx.ast.plain_export_named_declaration(SPAN, self.ctx.ast.new_vec(), None),
// ),
// ));
if module_count == 0 && removed_count > 0 {
let export_decl = ModuleDeclaration::ExportNamedDeclaration(
self.ctx.ast.plain_export_named_declaration(SPAN, self.ctx.ast.new_vec(), None),
);
program.body.push(self.ctx.ast.module_declaration(export_decl));
}
}

Expand Down Expand Up @@ -172,11 +220,6 @@ impl<'a> TypeScriptAnnotations<'a> {
});
}

pub fn transform_export_named_declaration(&mut self, decl: &mut ExportNamedDeclaration<'a>) {
// Remove type only specifiers
decl.specifiers.retain(|spec| !spec.export_kind.is_type());
}

pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
*expr = self.ctx.ast.copy(expr.get_inner_expression());
}
Expand All @@ -196,16 +239,6 @@ impl<'a> TypeScriptAnnotations<'a> {
func.modifiers.remove_type_modifiers();
}

pub fn transform_import_declaration(&mut self, decl: &mut ImportDeclaration<'a>) {
// Remove type only specifiers
if let Some(specifiers) = &mut decl.specifiers {
specifiers.retain(|spec| match spec {
ImportDeclarationSpecifier::ImportSpecifier(inner) => !inner.import_kind.is_type(),
_ => true,
});
}
}

pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
elem.type_parameters = None;
}
Expand Down Expand Up @@ -284,4 +317,12 @@ impl<'a> TypeScriptAnnotations<'a> {
) {
expr.type_parameters = None;
}

pub fn transform_statement(&mut self, decl: &mut Statement<'a>) {
if let Statement::ModuleDeclaration(module_decl) = decl {
if let ModuleDeclaration::TSExportAssignment(exp) = &mut **module_decl {
*decl = self.create_module_exports(exp);
}
}
}
}
36 changes: 36 additions & 0 deletions crates/oxc_transformer/src/typescript/collector.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use oxc_ast::ast::{ExportNamedDeclaration, IdentifierReference};
use oxc_span::Atom;
use rustc_hash::FxHashSet;

/// Collects identifier references
/// Indicates whether the BindingIdentifier is referenced or used in the ExportNamedDeclaration
#[derive(Debug)]
pub struct TypeScriptReferenceCollector<'a> {
names: FxHashSet<Atom<'a>>,
}

impl<'a> TypeScriptReferenceCollector<'a> {
pub fn new() -> Self {
Self { names: FxHashSet::default() }
}

pub fn has_reference(&self, name: &Atom) -> bool {
self.names.contains(name)
}

pub fn visit_identifier_reference(&mut self, ident: &IdentifierReference<'a>) {
self.names.insert(ident.name.clone());
}

pub fn visit_transform_export_named_declaration(&mut self, decl: &ExportNamedDeclaration<'a>) {
if decl.export_kind.is_type() {
return;
}

for specifier in &decl.specifiers {
if specifier.export_kind.is_value() {
self.names.insert(specifier.local.name().clone());
}
}
}
}
27 changes: 19 additions & 8 deletions crates/oxc_transformer/src/typescript/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod annotations;
mod collector;
mod namespace;

use std::rc::Rc;
Expand All @@ -10,11 +11,15 @@ use oxc_ast::ast::*;

use crate::context::Ctx;

use self::annotations::TypeScriptAnnotations;
use self::{annotations::TypeScriptAnnotations, collector::TypeScriptReferenceCollector};

#[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TypeScriptOptions;
pub struct TypeScriptOptions {
/// When set to true, the transform will only remove type-only imports (introduced in TypeScript 3.8).
/// This should only be used if you are using TypeScript >= 3.8.
only_remove_type_imports: bool,
}

/// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript)
///
Expand Down Expand Up @@ -43,6 +48,7 @@ pub struct TypeScript<'a> {
ctx: Ctx<'a>,

annotations: TypeScriptAnnotations<'a>,
reference_collector: TypeScriptReferenceCollector<'a>,
}

impl<'a> TypeScript<'a> {
Expand All @@ -51,6 +57,7 @@ impl<'a> TypeScript<'a> {

Self {
annotations: TypeScriptAnnotations::new(&options, ctx),
reference_collector: TypeScriptReferenceCollector::new(),
options,
ctx: Rc::clone(ctx),
}
Expand All @@ -60,7 +67,7 @@ impl<'a> TypeScript<'a> {
// Transforms
impl<'a> TypeScript<'a> {
pub fn transform_program_on_exit(&self, program: &mut Program<'a>) {
self.annotations.transform_program_on_exit(program);
self.annotations.transform_program_on_exit(program, &self.reference_collector);
}

pub fn transform_arrow_expression(&mut self, expr: &mut ArrowFunctionExpression<'a>) {
Expand All @@ -84,7 +91,7 @@ impl<'a> TypeScript<'a> {
}

pub fn transform_export_named_declaration(&mut self, decl: &mut ExportNamedDeclaration<'a>) {
self.annotations.transform_export_named_declaration(decl);
self.reference_collector.visit_transform_export_named_declaration(decl);
}

pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
Expand All @@ -103,10 +110,6 @@ impl<'a> TypeScript<'a> {
self.annotations.transform_function(func, flags);
}

pub fn transform_import_declaration(&mut self, decl: &mut ImportDeclaration<'a>) {
self.annotations.transform_import_declaration(decl);
}

pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
self.annotations.transform_jsx_opening_element(elem);
}
Expand Down Expand Up @@ -137,4 +140,12 @@ impl<'a> TypeScript<'a> {
) {
self.annotations.transform_tagged_template_expression(expr);
}

pub fn transform_identifier_reference(&mut self, ident: &mut IdentifierReference<'a>) {
self.reference_collector.visit_identifier_reference(ident);
}

pub fn transform_statement(&mut self, stmt: &mut Statement<'a>) {
self.annotations.transform_statement(stmt);
}
}
Loading

0 comments on commit df6d20c

Please sign in to comment.