mirror of
https://github.com/danbulant/oxc
synced 2026-05-25 04:42:10 +00:00
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:
parent
ac37d55600
commit
6a53fa367b
5 changed files with 174 additions and 108 deletions
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
@ -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 mut removed_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,
|
||||
program.body.retain_mut(|stmt| {
|
||||
let Statement::ModuleDeclaration(module_decl) = stmt else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Replace with `module.exports = expr`
|
||||
ModuleDeclaration::TSExportAssignment(exp) => {
|
||||
return Some(self.create_module_exports(exp));
|
||||
}
|
||||
};
|
||||
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()))
|
||||
});
|
||||
|
||||
if keep {
|
||||
module_count += 1;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
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();
|
||||
|
||||
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
|
||||
// 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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
crates/oxc_transformer/src/typescript/collector.rs
Normal file
36
crates/oxc_transformer/src/typescript/collector.rs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
mod annotations;
|
||||
mod collector;
|
||||
mod namespace;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
|
@ -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)
|
||||
///
|
||||
|
|
@ -43,6 +48,7 @@ pub struct TypeScript<'a> {
|
|||
ctx: Ctx<'a>,
|
||||
|
||||
annotations: TypeScriptAnnotations<'a>,
|
||||
reference_collector: TypeScriptReferenceCollector<'a>,
|
||||
}
|
||||
|
||||
impl<'a> TypeScript<'a> {
|
||||
|
|
@ -51,6 +57,7 @@ impl<'a> TypeScript<'a> {
|
|||
|
||||
Self {
|
||||
annotations: TypeScriptAnnotations::new(&options, ctx),
|
||||
reference_collector: TypeScriptReferenceCollector::new(),
|
||||
options,
|
||||
ctx: Rc::clone(ctx),
|
||||
}
|
||||
|
|
@ -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>) {
|
||||
|
|
@ -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>) {
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Passed: 93/227
|
||||
Passed: 123/227
|
||||
|
||||
# All Passed:
|
||||
* babel-plugin-transform-react-jsx-source
|
||||
|
|
@ -24,7 +24,7 @@ Passed: 93/227
|
|||
* opts/optimizeConstEnums/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-true/input.ts
|
||||
* class/abstract-class-decorated/input.ts
|
||||
|
|
@ -40,48 +40,18 @@ Passed: 93/227
|
|||
* class/private-method-override-transform-private/input.ts
|
||||
* class/transform-properties-declare-wrong-order/input.ts
|
||||
* declarations/erased/input.ts
|
||||
* declarations/export-declare-enum/input.ts
|
||||
* declarations/nested-namespace/input.mjs
|
||||
* exports/declare-namespace/input.ts
|
||||
* exports/declare-shadowed/input.ts
|
||||
* exports/declared-types/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=/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
|
||||
* imports/elide-preact/input.ts
|
||||
* imports/elide-react/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-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/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-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/ambient-module-nested/input.ts
|
||||
* namespace/ambient-module-nested-exported/input.ts
|
||||
|
|
|
|||
Loading…
Reference in a new issue