feat(transformer/typescript): correct elide imports/exports statements (#2995)

remove ts annotations one benefit: `IdentifierReference` only used on js
code

The `TypescriptReferenceCollector` implementation is inspired by
5f75019683/crates/swc_ecma_transforms_typescript/src/strip_import_export.rs (L9-L99)

This seems simpler to implement than using scope
This commit is contained in:
Dunqing 2024-04-16 11:06:58 +08:00 committed by GitHub
parent ac37d55600
commit 6a53fa367b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 174 additions and 108 deletions

View file

@ -143,8 +143,6 @@ impl<'a> VisitMut<'a> for Transformer<'a> {
} }
fn visit_import_declaration(&mut self, decl: &mut ImportDeclaration<'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); walk_mut::walk_import_declaration_mut(self, decl);
} }
@ -198,4 +196,14 @@ impl<'a> VisitMut<'a> for Transformer<'a> {
walk_mut::walk_variable_declarator_mut(self, declarator); 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);
}
} }

View file

@ -11,25 +11,17 @@ use oxc_span::{Atom, SPAN};
use oxc_syntax::operator::AssignmentOperator; use oxc_syntax::operator::AssignmentOperator;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use super::collector::TypeScriptReferenceCollector;
pub struct TypeScriptAnnotations<'a> { pub struct TypeScriptAnnotations<'a> {
#[allow(dead_code)] #[allow(dead_code)]
options: Rc<TypeScriptOptions>, options: Rc<TypeScriptOptions>,
ctx: Ctx<'a>, ctx: Ctx<'a>,
global_types: FxHashSet<String>,
} }
impl<'a> TypeScriptAnnotations<'a> { impl<'a> TypeScriptAnnotations<'a> {
pub fn new(options: &Rc<TypeScriptOptions>, ctx: &Ctx<'a>) -> Self { pub fn new(options: &Rc<TypeScriptOptions>, ctx: &Ctx<'a>) -> Self {
Self { Self { options: Rc::clone(options), ctx: Rc::clone(ctx) }
options: Rc::clone(options),
ctx: Rc::clone(ctx),
global_types: FxHashSet::default(),
}
}
pub fn is_global_type(&self) -> bool {
self.global_types.contains("TODO")
} }
// Convert `export = expr` into `module.exports = expr` // Convert `export = expr` into `module.exports = expr`
@ -73,57 +65,113 @@ impl<'a> TypeScriptAnnotations<'a> {
} }
// Remove type only imports/exports // 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 mut module_count = 0;
let mut removed_count = 0;
let body = program.body.retain_mut(|stmt| {
self.ctx.ast.move_statement_vec(&mut program.body).into_iter().filter_map(|stmt| { let Statement::ModuleDeclaration(module_decl) = stmt else {
// If an import/export declaration, remove all that are type-only return true;
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` let need_delete = match &mut **module_decl {
ModuleDeclaration::TSExportAssignment(exp) => { ModuleDeclaration::ExportNamedDeclaration(decl) => {
return Some(self.create_module_exports(exp)); decl.specifiers.retain(|specifier| {
} !(specifier.export_kind.is_type()
}; || import_type_names.contains(specifier.exported.name()))
});
if keep { decl.export_kind.is_type()
module_count += 1; || ((decl.declaration.is_none()
} else { || decl.declaration.as_ref().is_some_and(|d| {
return None; 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();
Some(stmt) let is_specifiers_empty =
}); decl.specifiers.as_ref().is_some_and(|s| s.is_empty());
program.body = self.ctx.ast.new_vec_from_iter(body); 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,
};
if need_delete {
removed_count += 1;
} else {
module_count += 1;
}
!need_delete
});
// Determine if we still have import/export statements, otherwise we // Determine if we still have import/export statements, otherwise we
// need to inject an empty statement (`export {}`) so that the file is // need to inject an empty statement (`export {}`) so that the file is
// still considered a module // still considered a module
if module_count == 0 && self.ctx.semantic.source_type().is_module() { if module_count == 0 && removed_count > 0 {
// FIXME let export_decl = ModuleDeclaration::ExportNamedDeclaration(
// program.body.push(self.ctx.ast.module_declaration( self.ctx.ast.plain_export_named_declaration(SPAN, self.ctx.ast.new_vec(), None),
// 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));
// ),
// ));
} }
} }
@ -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>) { pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
*expr = self.ctx.ast.copy(expr.get_inner_expression()); *expr = self.ctx.ast.copy(expr.get_inner_expression());
} }
@ -196,16 +239,6 @@ impl<'a> TypeScriptAnnotations<'a> {
func.modifiers.remove_type_modifiers(); 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>) { pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
elem.type_parameters = None; elem.type_parameters = None;
} }
@ -284,4 +317,12 @@ impl<'a> TypeScriptAnnotations<'a> {
) { ) {
expr.type_parameters = None; 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);
}
}
}
} }

View file

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

View file

@ -1,4 +1,5 @@
mod annotations; mod annotations;
mod collector;
mod namespace; mod namespace;
use std::rc::Rc; use std::rc::Rc;
@ -10,11 +11,15 @@ use oxc_ast::ast::*;
use crate::context::Ctx; use crate::context::Ctx;
use self::annotations::TypeScriptAnnotations; use self::{annotations::TypeScriptAnnotations, collector::TypeScriptReferenceCollector};
#[derive(Debug, Default, Clone, Deserialize)] #[derive(Debug, Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")] #[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) /// [Preset TypeScript](https://babeljs.io/docs/babel-preset-typescript)
/// ///
@ -43,6 +48,7 @@ pub struct TypeScript<'a> {
ctx: Ctx<'a>, ctx: Ctx<'a>,
annotations: TypeScriptAnnotations<'a>, annotations: TypeScriptAnnotations<'a>,
reference_collector: TypeScriptReferenceCollector<'a>,
} }
impl<'a> TypeScript<'a> { impl<'a> TypeScript<'a> {
@ -51,6 +57,7 @@ impl<'a> TypeScript<'a> {
Self { Self {
annotations: TypeScriptAnnotations::new(&options, ctx), annotations: TypeScriptAnnotations::new(&options, ctx),
reference_collector: TypeScriptReferenceCollector::new(),
options, options,
ctx: Rc::clone(ctx), ctx: Rc::clone(ctx),
} }
@ -60,7 +67,7 @@ impl<'a> TypeScript<'a> {
// Transforms // Transforms
impl<'a> TypeScript<'a> { impl<'a> TypeScript<'a> {
pub fn transform_program_on_exit(&self, program: &mut Program<'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>) { pub fn transform_arrow_expression(&mut self, expr: &mut ArrowFunctionExpression<'a>) {
@ -84,7 +91,7 @@ impl<'a> TypeScript<'a> {
} }
pub fn transform_export_named_declaration(&mut self, decl: &mut ExportNamedDeclaration<'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>) { pub fn transform_expression(&mut self, expr: &mut Expression<'a>) {
@ -103,10 +110,6 @@ impl<'a> TypeScript<'a> {
self.annotations.transform_function(func, flags); 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>) { pub fn transform_jsx_opening_element(&mut self, elem: &mut JSXOpeningElement<'a>) {
self.annotations.transform_jsx_opening_element(elem); self.annotations.transform_jsx_opening_element(elem);
} }
@ -137,4 +140,12 @@ impl<'a> TypeScript<'a> {
) { ) {
self.annotations.transform_tagged_template_expression(expr); 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);
}
} }

View file

@ -1,4 +1,4 @@
Passed: 93/227 Passed: 123/227
# All Passed: # All Passed:
* babel-plugin-transform-react-jsx-source * babel-plugin-transform-react-jsx-source
@ -24,7 +24,7 @@ Passed: 93/227
* opts/optimizeConstEnums/input.ts * opts/optimizeConstEnums/input.ts
* opts/rewriteImportExtensions/input.ts * opts/rewriteImportExtensions/input.ts
# babel-plugin-transform-typescript (49/147) # babel-plugin-transform-typescript (79/147)
* class/abstract-allowDeclareFields-false/input.ts * class/abstract-allowDeclareFields-false/input.ts
* class/abstract-allowDeclareFields-true/input.ts * class/abstract-allowDeclareFields-true/input.ts
* class/abstract-class-decorated/input.ts * class/abstract-class-decorated/input.ts
@ -40,48 +40,18 @@ Passed: 93/227
* class/private-method-override-transform-private/input.ts * class/private-method-override-transform-private/input.ts
* class/transform-properties-declare-wrong-order/input.ts * class/transform-properties-declare-wrong-order/input.ts
* declarations/erased/input.ts * declarations/erased/input.ts
* declarations/export-declare-enum/input.ts
* declarations/nested-namespace/input.mjs * declarations/nested-namespace/input.mjs
* exports/declare-namespace/input.ts
* exports/declare-shadowed/input.ts * exports/declare-shadowed/input.ts
* exports/declared-types/input.ts * exports/declared-types/input.ts
* exports/export-const-enums/input.ts * exports/export-const-enums/input.ts
* exports/export-type/input.ts
* exports/export-type-from/input.ts
* exports/export-type-star-from/input.ts * exports/export-type-star-from/input.ts
* exports/export=/input.ts * exports/export=/input.ts
* exports/imported-types/input.ts
* exports/imported-types-only-remove-type-imports/input.ts
* exports/issue-9916-3/input.ts
* exports/simple/input.ts
* exports/type-only-export-specifier-1/input.ts
* exports/type-only-export-specifier-2/input.ts
* function/overloads-exports/input.mjs * function/overloads-exports/input.mjs
* imports/elide-preact/input.ts
* imports/elide-react/input.ts
* imports/elide-type-referenced-in-imports-equal-no/input.ts * imports/elide-type-referenced-in-imports-equal-no/input.ts
* imports/elide-typeof/input.ts
* imports/elision/input.ts
* imports/elision-export-type/input.ts
* imports/elision-locations/input.ts
* imports/elision-qualifiedname/input.ts
* imports/elision-rename/input.ts
* imports/enum-id/input.ts * imports/enum-id/input.ts
* imports/enum-value/input.ts * imports/enum-value/input.ts
* imports/import-named-type/input.ts
* imports/import-named-type-default-and-named/input.ts
* imports/import-removed-exceptions/input.ts
* imports/import-type/input.ts
* imports/import-type-func-with-duplicate-name/input.ts
* imports/import-type-not-removed/input.ts
* imports/import=-module/input.ts * imports/import=-module/input.ts
* imports/only-remove-type-imports/input.ts
* imports/property-signature/input.ts
* imports/type-only-export-specifier-1/input.ts
* imports/type-only-export-specifier-2/input.ts * imports/type-only-export-specifier-2/input.ts
* imports/type-only-import-specifier-2/input.ts
* imports/type-only-import-specifier-3/input.ts
* imports/type-only-import-specifier-4/input.ts
* namespace/alias/input.ts * namespace/alias/input.ts
* namespace/ambient-module-nested/input.ts * namespace/ambient-module-nested/input.ts
* namespace/ambient-module-nested-exported/input.ts * namespace/ambient-module-nested-exported/input.ts