From 95d741abd65a1de3cfbdf0e7ecbf200432c27cd8 Mon Sep 17 00:00:00 2001 From: Dunqing Date: Wed, 17 Jan 2024 15:58:04 +0800 Subject: [PATCH] feat(transformer/typescript): remove type only imports/exports correctly (#2055) --- crates/oxc_transformer/src/typescript/mod.rs | 163 +++++++++++++------ tasks/transform_conformance/babel.snap.md | 6 +- 2 files changed, 115 insertions(+), 54 deletions(-) diff --git a/crates/oxc_transformer/src/typescript/mod.rs b/crates/oxc_transformer/src/typescript/mod.rs index bb7f21042..14f5a6ba2 100644 --- a/crates/oxc_transformer/src/typescript/mod.rs +++ b/crates/oxc_transformer/src/typescript/mod.rs @@ -1,5 +1,6 @@ use oxc_allocator::{Box, Vec}; use oxc_ast::{ast::*, AstBuilder}; +use oxc_semantic::SymbolFlags; use oxc_span::{Atom, SPAN}; use oxc_syntax::{ operator::{AssignmentOperator, BinaryOperator, LogicalOperator}, @@ -20,8 +21,6 @@ pub struct TypeScript<'a> { ast: Rc>, ctx: TransformerCtx<'a>, verbatim_module_syntax: bool, - /// type imports names - import_type_name_set: FxHashSet, export_name_set: FxHashSet, } @@ -31,13 +30,7 @@ impl<'a> TypeScript<'a> { ctx: TransformerCtx<'a>, verbatim_module_syntax: bool, ) -> Self { - Self { - ast, - ctx, - verbatim_module_syntax, - import_type_name_set: FxHashSet::default(), - export_name_set: FxHashSet::default(), - } + Self { ast, ctx, verbatim_module_syntax, export_name_set: FxHashSet::default() } } pub fn transform_declaration(&mut self, decl: &mut Declaration<'a>) { @@ -109,24 +102,84 @@ impl<'a> TypeScript<'a> { /// * Adds `export {}` if all import / export statements are removed, this is used to tell /// downstream tools that this file is in ESM. pub fn transform_program(&mut self, program: &mut Program<'a>) { - let mut needs_explicit_esm = false; + let mut export_type_names = FxHashSet::default(); + let mut export_names = FxHashSet::default(); - for stmt in program.body.iter_mut() { + // Collect export names + program.body.iter().for_each(|stmt| { if let Statement::ModuleDeclaration(module_decl) = stmt { - needs_explicit_esm = true; + match &**module_decl { + ModuleDeclaration::ExportNamedDeclaration(decl) => { + decl.specifiers.iter().for_each(|specifier| { + let name = specifier.exported.name(); + if self.is_import_binding_only(name) { + let is_value = + decl.export_kind.is_value() && specifier.export_kind.is_value(); + if is_value { + export_names.insert(name.clone()); + } else { + export_type_names.insert(name.clone()); + } + } + }); + } + ModuleDeclaration::ExportDefaultDeclaration(decl) => { + let name = decl.exported.name(); + if self.is_import_binding_only(name) { + export_names.insert(decl.exported.name().clone()); + } + } + ModuleDeclaration::ExportAllDeclaration(decl) => { + if let Some(exported) = &decl.exported { + let name = exported.name(); + if self.is_import_binding_only(name) { + let is_value = + decl.export_kind.is_value() && decl.export_kind.is_value(); + if is_value { + export_names.insert(name.clone()); + } else { + export_type_names.insert(name.clone()); + } + } + } + } + _ => {} + } + } + }); + + let mut import_type_names = FxHashSet::default(); + let mut delete_indexes = vec![]; + let mut import_len = 0; + + for (index, stmt) in program.body.iter_mut().enumerate() { + if let Statement::ModuleDeclaration(module_decl) = stmt { + import_len += 1; match &mut **module_decl { ModuleDeclaration::ExportNamedDeclaration(decl) => { decl.specifiers.retain(|specifier| { !(specifier.export_kind.is_type() - || self.import_type_name_set.contains(specifier.exported.name())) + || import_type_names.contains(specifier.exported.name())) }); + + if decl.export_kind.is_type() + || self.verbatim_module_syntax + || (decl.declaration.is_none() && decl.specifiers.is_empty()) + { + delete_indexes.push(index); + } } - ModuleDeclaration::ImportDeclaration(decl) if decl.import_kind.is_value() => { + ModuleDeclaration::ImportDeclaration(decl) => { + let is_type = decl.import_kind.is_type(); if let Some(specifiers) = &mut decl.specifiers { specifiers.retain(|specifier| match specifier { ImportDeclarationSpecifier::ImportSpecifier(s) => { - if s.import_kind.is_type() { - self.import_type_name_set.insert(s.local.name.clone()); + if is_type || s.import_kind.is_type() { + import_type_names.insert(s.local.name.clone()); + return false; + } + + if export_type_names.contains(&s.local.name) { return false; } @@ -135,62 +188,55 @@ impl<'a> TypeScript<'a> { } self.has_value_references(&s.local.name) + || export_names.contains(&s.local.name) } ImportDeclarationSpecifier::ImportDefaultSpecifier(s) if !self.verbatim_module_syntax => { + if is_type { + import_type_names.insert(s.local.name.clone()); + } + self.has_value_references(&s.local.name) + || export_names.contains(&s.local.name) } ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) if !self.verbatim_module_syntax => { + if is_type { + import_type_names.insert(s.local.name.clone()); + } + self.has_value_references(&s.local.name) + || export_names.contains(&s.local.name) } _ => true, }); } + + if decl.import_kind.is_type() + || decl + .specifiers + .as_ref() + .is_some_and(|specifiers| specifiers.is_empty()) + { + delete_indexes.push(index); + } } _ => {} } } } - program.body.retain(|stmt| match stmt { - Statement::ModuleDeclaration(module_decl) => match &**module_decl { - ModuleDeclaration::ImportDeclaration(decl) => { - if decl.import_kind.is_type() { - return false; - } + let delete_indexes_len = delete_indexes.len(); - if self.verbatim_module_syntax { - return true; - } + // remove empty imports/exports + for index in delete_indexes.into_iter().rev() { + program.body.remove(index); + } - !decl.specifiers.as_ref().is_some_and(|specifiers| specifiers.is_empty()) - } - ModuleDeclaration::ExportNamedDeclaration(decl) => { - if decl.export_kind.is_type() { - return false; - } - - if self.verbatim_module_syntax { - return true; - } - - if decl.declaration.is_none() && decl.specifiers.is_empty() { - return false; - } - - true - } - _ => true, - }, - _ => true, - }); - - if needs_explicit_esm - && !program.body.iter().any(|s| matches!(s, Statement::ModuleDeclaration(_))) - { + // explicit esm + if import_len > 0 && import_len == delete_indexes_len { let empty_export = self.ast.export_named_declaration( SPAN, None, @@ -203,6 +249,23 @@ impl<'a> TypeScript<'a> { } } + /// ```ts + /// import foo from "foo"; // is import binding only + /// import bar from "bar"; // SymbolFlags::ImportBinding | SymbolFlags::BlockScopedVariable + /// let bar = "xx"; + /// ``` + fn is_import_binding_only(&self, name: &Atom) -> bool { + let root_scope_id = self.ctx.scopes().root_scope_id(); + + self.ctx.scopes().get_binding(root_scope_id, name).is_some_and(|symbol_id| { + let flag = self.ctx.symbols().get_flag(symbol_id); + flag.is_import_binding() + && !flag.intersects( + SymbolFlags::FunctionScopedVariable | SymbolFlags::BlockScopedVariable, + ) + }) + } + fn has_value_references(&self, name: &Atom) -> bool { let root_scope_id = self.ctx.scopes().root_scope_id(); diff --git a/tasks/transform_conformance/babel.snap.md b/tasks/transform_conformance/babel.snap.md index 66f8f908d..293235781 100644 --- a/tasks/transform_conformance/babel.snap.md +++ b/tasks/transform_conformance/babel.snap.md @@ -1,4 +1,4 @@ -Passed: 306/1179 +Passed: 308/1179 # All Passed: * babel-plugin-transform-numeric-separator @@ -832,7 +832,7 @@ Passed: 306/1179 * general/function-duplicate-name/input.js * general/object/input.js -# babel-plugin-transform-typescript (75/158) +# babel-plugin-transform-typescript (77/158) * class/abstract-class-decorated/input.ts * class/abstract-class-decorated-method/input.ts * class/abstract-class-decorated-parameter/input.ts @@ -856,8 +856,6 @@ Passed: 306/1179 * exports/export-type-star-from/input.ts * exports/export=/input.ts * exports/export=-to-cjs/input.ts -* exports/imported-types/input.ts -* exports/imported-types-only-remove-type-imports/input.ts * exports/issue-9916-3/input.ts * function/overloads-exports/input.mjs * imports/elide-injected/input.ts