Skip to main content

oxc_transformer/typescript/
annotations.rs

1use oxc_allocator::{TakeIn, Vec as ArenaVec};
2use oxc_ast::ast::*;
3use oxc_diagnostics::OxcDiagnostic;
4use oxc_semantic::{Reference, SymbolFlags};
5use oxc_span::{GetSpan, SPAN, Span, Str};
6use oxc_syntax::{
7    operator::AssignmentOperator,
8    reference::ReferenceFlags,
9    scope::{ScopeFlags, ScopeId},
10    symbol::SymbolId,
11};
12use oxc_traverse::Traverse;
13
14use crate::{TypeScriptOptions, context::TraverseCtx, state::TransformState};
15
16pub struct TypeScriptAnnotations<'a> {
17    // Options
18    only_remove_type_imports: bool,
19
20    /// Assignments to be added to the constructor body
21    assignments: Vec<Assignment<'a>>,
22    has_super_call: bool,
23
24    has_jsx_element: bool,
25    has_jsx_fragment: bool,
26    jsx_element_import_name: String,
27    jsx_fragment_import_name: String,
28}
29
30impl TypeScriptAnnotations<'_> {
31    pub fn new(options: &TypeScriptOptions) -> Self {
32        let jsx_element_import_name = if options.jsx_pragma.contains('.') {
33            options.jsx_pragma.split('.').next().map(String::from).unwrap()
34        } else {
35            options.jsx_pragma.to_string()
36        };
37
38        let jsx_fragment_import_name = if options.jsx_pragma_frag.contains('.') {
39            options.jsx_pragma_frag.split('.').next().map(String::from).unwrap()
40        } else {
41            options.jsx_pragma_frag.to_string()
42        };
43
44        Self {
45            only_remove_type_imports: options.only_remove_type_imports,
46            has_super_call: false,
47            assignments: vec![],
48            has_jsx_element: false,
49            has_jsx_fragment: false,
50            jsx_element_import_name,
51            jsx_fragment_import_name,
52        }
53    }
54}
55
56impl<'a> Traverse<'a, TransformState<'a>> for TypeScriptAnnotations<'a> {
57    fn exit_program(&mut self, program: &mut Program<'a>, ctx: &mut TraverseCtx<'a>) {
58        let mut no_modules_remaining = true;
59        let mut some_modules_deleted = false;
60
61        program.body.retain_mut(|stmt| {
62            let need_retain = match stmt {
63                Statement::ExportNamedDeclaration(decl) if decl.declaration.is_some() => {
64                    decl.declaration.as_ref().is_some_and(|decl| !decl.is_typescript_syntax())
65                }
66                Statement::ExportNamedDeclaration(decl) => {
67                    if decl.export_kind.is_type() {
68                        false
69                    } else if decl.specifiers.is_empty() {
70                        // `export {}` or `export {} from 'mod'`
71                        // Keep the export declaration if there are no export specifiers
72                        true
73                    } else {
74                        decl.specifiers
75                            .retain(|specifier| Self::can_retain_export_specifier(specifier, ctx));
76                        // Keep the export declaration if there are still specifiers after removing type exports
77                        !decl.specifiers.is_empty()
78                    }
79                }
80                Statement::ExportAllDeclaration(decl) => !decl.export_kind.is_type(),
81                Statement::ExportDefaultDeclaration(decl) => {
82                    !decl.is_typescript_syntax()
83                        && !matches!(
84                            &decl.declaration,
85                            ExportDefaultDeclarationKind::Identifier(ident) if Self::is_refers_to_type(ident, ctx)
86                        )
87                }
88                Statement::ImportDeclaration(decl) => {
89                    if decl.import_kind.is_type() {
90                        false
91                    } else if let Some(specifiers) = &mut decl.specifiers {
92                        if specifiers.is_empty() {
93                            // import {} from 'mod' -> import 'mod'
94                            decl.specifiers = None;
95                            true
96                        } else {
97                            specifiers.retain(|specifier| {
98                                let id = match specifier {
99                                    ImportDeclarationSpecifier::ImportSpecifier(s) => {
100                                        if s.import_kind.is_type() {
101                                            return false;
102                                        }
103                                        &s.local
104                                    }
105                                    ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
106                                        &s.local
107                                    }
108                                    ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
109                                        &s.local
110                                    }
111                                };
112                                // If `only_remove_type_imports` is true, then we can return `true` to keep it because
113                                // it is not a type import, otherwise we need to check if the identifier is referenced
114                                if self.only_remove_type_imports {
115                                    true
116                                } else {
117                                    self.has_value_reference(id, ctx)
118                                }
119                            });
120
121                            if specifiers.is_empty() {
122                                // `import { type A } from 'mod'`
123                                if self.only_remove_type_imports {
124                                    // -> `import 'mod'`
125                                    decl.specifiers = None;
126                                    true
127                                } else {
128                                    // Remove the import declaration if all specifiers are removed
129                                    false
130                                }
131                            } else {
132                                true
133                            }
134                        }
135                    } else {
136                        true
137                    }
138                }
139                // `import Binding = X.Y.Z`
140                // `Binding` can be referenced as a value or a type, but here we already know it only as a type
141                // See `TypeScriptModule::transform_ts_import_equals`
142                Statement::TSTypeAliasDeclaration(_)
143                | Statement::TSExportAssignment(_)
144                | Statement::TSNamespaceExportDeclaration(_) => false,
145                _ => return true,
146            };
147
148            if need_retain {
149                no_modules_remaining = false;
150            } else {
151                some_modules_deleted = true;
152            }
153
154            need_retain
155        });
156
157        // Determine if we still have import/export statements, otherwise we
158        // need to inject an empty statement (`export {}`) so that the file is
159        // still considered a module
160        if no_modules_remaining && some_modules_deleted && ctx.state.module_imports.is_empty() {
161            let export_decl = Statement::ExportNamedDeclaration(
162                ctx.ast.plain_export_named_declaration(SPAN, ctx.ast.vec(), None),
163            );
164            program.body.push(export_decl);
165        }
166    }
167
168    fn enter_arrow_function_expression(
169        &mut self,
170        expr: &mut ArrowFunctionExpression<'a>,
171        _ctx: &mut TraverseCtx<'a>,
172    ) {
173        expr.type_parameters = None;
174        expr.return_type = None;
175    }
176
177    fn enter_variable_declarator(
178        &mut self,
179        decl: &mut VariableDeclarator<'a>,
180        _ctx: &mut TraverseCtx<'a>,
181    ) {
182        decl.definite = false;
183        decl.type_annotation = None;
184    }
185
186    fn enter_call_expression(&mut self, expr: &mut CallExpression<'a>, _ctx: &mut TraverseCtx<'a>) {
187        expr.type_arguments = None;
188    }
189
190    fn enter_chain_element(&mut self, element: &mut ChainElement<'a>, ctx: &mut TraverseCtx<'a>) {
191        if let ChainElement::TSNonNullExpression(e) = element {
192            *element = match e.expression.get_inner_expression_mut().take_in(ctx.ast) {
193                Expression::CallExpression(call_expr) => ChainElement::CallExpression(call_expr),
194                expr @ match_member_expression!(Expression) => {
195                    ChainElement::from(expr.into_member_expression())
196                }
197                _ => {
198                    /* syntax error */
199                    return;
200                }
201            }
202        }
203    }
204
205    fn enter_function(&mut self, func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) {
206        // Remove TypeScript annotations from function declarations
207        // Note: declare flag is preserved for exit_statements to handle declaration removal
208        func.type_parameters = None;
209        func.return_type = None;
210        func.this_param = None;
211    }
212
213    fn enter_class(&mut self, class: &mut Class<'a>, _ctx: &mut TraverseCtx<'a>) {
214        // Remove TypeScript annotations from class declarations
215        // Note: declare flag is preserved for exit_statements to handle declaration removal
216        class.type_parameters = None;
217        class.super_type_arguments = None;
218        class.implements.clear();
219        class.r#abstract = false;
220
221        // Remove type only members
222        class.body.body.retain(|elem| match elem {
223            ClassElement::MethodDefinition(method) => {
224                matches!(method.r#type, MethodDefinitionType::MethodDefinition)
225                    && !method.value.is_typescript_syntax()
226            }
227            ClassElement::PropertyDefinition(prop) => {
228                matches!(prop.r#type, PropertyDefinitionType::PropertyDefinition)
229            }
230            ClassElement::AccessorProperty(prop) => {
231                matches!(prop.r#type, AccessorPropertyType::AccessorProperty)
232            }
233            ClassElement::TSIndexSignature(_) => false,
234            ClassElement::StaticBlock(_) => true,
235        });
236    }
237
238    fn exit_class(&mut self, class: &mut Class<'a>, _: &mut TraverseCtx<'a>) {
239        // Remove `declare` properties from the class body, other ts-only properties have been removed in `enter_class`.
240        // The reason that removing `declare` properties here because the legacy-decorator plugin needs to transform
241        // `declare` field in the `exit_class` phase, so we have to ensure this step is run after the legacy-decorator plugin.
242        class
243            .body
244            .body
245            .retain(|elem| !matches!(elem, ClassElement::PropertyDefinition(prop) if prop.declare));
246    }
247
248    fn enter_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a>) {
249        if expr.is_typescript_syntax() {
250            let inner_expr = expr.get_inner_expression_mut();
251            *expr = inner_expr.take_in(ctx.ast);
252        }
253    }
254
255    fn enter_simple_assignment_target(
256        &mut self,
257        target: &mut SimpleAssignmentTarget<'a>,
258        ctx: &mut TraverseCtx<'a>,
259    ) {
260        if let Some(expr) = target.get_expression_mut() {
261            match expr.get_inner_expression_mut() {
262                // `foo!++` to `foo++`
263                inner_expr @ Expression::Identifier(_) => {
264                    let inner_expr = inner_expr.take_in(ctx.ast);
265                    let Expression::Identifier(ident) = inner_expr else {
266                        unreachable!();
267                    };
268                    *target = SimpleAssignmentTarget::AssignmentTargetIdentifier(ident);
269                }
270                // `foo.bar!++` to `foo.bar++`
271                inner_expr @ match_member_expression!(Expression) => {
272                    let inner_expr = inner_expr.take_in(ctx.ast);
273                    let member_expr = inner_expr.into_member_expression();
274                    *target = SimpleAssignmentTarget::from(member_expr);
275                }
276                _ => {
277                    // This should be never hit until more syntax is added to the JavaScript/TypeScrips
278                    ctx.state.error(OxcDiagnostic::error("Cannot strip out typescript syntax if SimpleAssignmentTarget is not an IdentifierReference or MemberExpression"));
279                }
280            }
281        }
282    }
283
284    fn enter_assignment_target(
285        &mut self,
286        target: &mut AssignmentTarget<'a>,
287        ctx: &mut TraverseCtx<'a>,
288    ) {
289        if let Some(expr) = target.get_expression_mut() {
290            let inner_expr = expr.get_inner_expression_mut();
291            if inner_expr.is_member_expression() {
292                let inner_expr = inner_expr.take_in(ctx.ast);
293                let member_expr = inner_expr.into_member_expression();
294                *target = AssignmentTarget::from(member_expr);
295            }
296        }
297    }
298
299    fn enter_formal_parameter(
300        &mut self,
301        param: &mut FormalParameter<'a>,
302        _ctx: &mut TraverseCtx<'a>,
303    ) {
304        param.accessibility = None;
305        param.readonly = false;
306        param.r#override = false;
307        param.optional = false;
308        param.type_annotation = None;
309    }
310
311    fn exit_function(&mut self, func: &mut Function<'a>, _ctx: &mut TraverseCtx<'a>) {
312        func.this_param = None;
313        func.type_parameters = None;
314        func.return_type = None;
315    }
316
317    fn enter_jsx_opening_element(
318        &mut self,
319        elem: &mut JSXOpeningElement<'a>,
320        _ctx: &mut TraverseCtx<'a>,
321    ) {
322        elem.type_arguments = None;
323    }
324
325    fn enter_method_definition(
326        &mut self,
327        def: &mut MethodDefinition<'a>,
328        _ctx: &mut TraverseCtx<'a>,
329    ) {
330        def.accessibility = None;
331        def.optional = false;
332        def.r#override = false;
333    }
334
335    fn enter_new_expression(&mut self, expr: &mut NewExpression<'a>, _ctx: &mut TraverseCtx<'a>) {
336        expr.type_arguments = None;
337    }
338
339    fn enter_property_definition(
340        &mut self,
341        def: &mut PropertyDefinition<'a>,
342        _ctx: &mut TraverseCtx<'a>,
343    ) {
344        def.accessibility = None;
345        def.definite = false;
346        def.r#override = false;
347        def.optional = false;
348        def.readonly = false;
349        def.type_annotation = None;
350    }
351
352    fn enter_accessor_property(
353        &mut self,
354        def: &mut AccessorProperty<'a>,
355        _ctx: &mut TraverseCtx<'a>,
356    ) {
357        def.accessibility = None;
358        def.definite = false;
359        def.type_annotation = None;
360    }
361
362    fn enter_statements(
363        &mut self,
364        stmts: &mut ArenaVec<'a, Statement<'a>>,
365        ctx: &mut TraverseCtx<'a>,
366    ) {
367        // Remove TS-only statements early to avoid traversing their children
368        stmts.retain(|stmt| match stmt {
369            match_declaration!(Statement) => {
370                self.should_keep_declaration(stmt.to_declaration(), ctx)
371            }
372            _ => true,
373        });
374    }
375
376    fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a>) {
377        // Add assignments after super calls
378        if self.assignments.is_empty() {
379            return;
380        }
381
382        let has_super_call = matches!(stmt, Statement::ExpressionStatement(stmt) if stmt.expression.is_super_call_expression());
383        if !has_super_call {
384            return;
385        }
386
387        // Add assignments after super calls
388        let assignments: Vec<_> = self
389            .assignments
390            .iter()
391            .map(|assignment| assignment.create_this_property_assignment(ctx))
392            .collect();
393        ctx.state.statement_injector.insert_many_after(stmt, assignments);
394        self.has_super_call = true;
395    }
396
397    /// Transform if statement's consequent and alternate to block statements if they are super calls
398    /// ```ts
399    /// if (true) super() else super();
400    /// // to
401    /// if (true) { super() } else { super() }
402    /// ```
403    fn enter_if_statement(&mut self, stmt: &mut IfStatement<'a>, ctx: &mut TraverseCtx<'a>) {
404        if !self.assignments.is_empty() {
405            let consequent_span = match &stmt.consequent {
406                Statement::ExpressionStatement(expr)
407                    if expr.expression.is_super_call_expression() =>
408                {
409                    Some(expr.span)
410                }
411                _ => None,
412            };
413            if let Some(span) = consequent_span {
414                let consequent = stmt.consequent.take_in(ctx.ast);
415                stmt.consequent = Self::create_block_with_statement(consequent, span, ctx);
416            }
417
418            let alternate_span = match &stmt.alternate {
419                Some(Statement::ExpressionStatement(expr))
420                    if expr.expression.is_super_call_expression() =>
421                {
422                    Some(expr.span)
423                }
424                _ => None,
425            };
426            if let Some(span) = alternate_span {
427                let alternate = stmt.alternate.take().unwrap();
428                stmt.alternate = Some(Self::create_block_with_statement(alternate, span, ctx));
429            }
430        }
431
432        Self::replace_with_empty_block_if_ts(&mut stmt.consequent, ctx.current_scope_id(), ctx);
433
434        if stmt.alternate.as_ref().is_some_and(Statement::is_typescript_syntax) {
435            stmt.alternate = None;
436        }
437    }
438
439    fn enter_for_statement(&mut self, stmt: &mut ForStatement<'a>, ctx: &mut TraverseCtx<'a>) {
440        let scope_id = stmt.scope_id();
441        Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx);
442    }
443
444    fn enter_for_in_statement(&mut self, stmt: &mut ForInStatement<'a>, ctx: &mut TraverseCtx<'a>) {
445        let scope_id = stmt.scope_id();
446        Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx);
447    }
448
449    fn enter_for_of_statement(&mut self, stmt: &mut ForOfStatement<'a>, ctx: &mut TraverseCtx<'a>) {
450        let scope_id = stmt.scope_id();
451        Self::replace_for_statement_body_with_empty_block_if_ts(&mut stmt.body, scope_id, ctx);
452    }
453
454    fn enter_while_statement(&mut self, stmt: &mut WhileStatement<'a>, ctx: &mut TraverseCtx<'a>) {
455        Self::replace_with_empty_block_if_ts(&mut stmt.body, ctx.current_scope_id(), ctx);
456    }
457
458    fn enter_do_while_statement(
459        &mut self,
460        stmt: &mut DoWhileStatement<'a>,
461        ctx: &mut TraverseCtx<'a>,
462    ) {
463        Self::replace_with_empty_block_if_ts(&mut stmt.body, ctx.current_scope_id(), ctx);
464    }
465
466    fn enter_tagged_template_expression(
467        &mut self,
468        expr: &mut TaggedTemplateExpression<'a>,
469        _ctx: &mut TraverseCtx<'a>,
470    ) {
471        expr.type_arguments = None;
472    }
473
474    fn enter_jsx_element(&mut self, _elem: &mut JSXElement<'a>, _ctx: &mut TraverseCtx<'a>) {
475        self.has_jsx_element = true;
476    }
477
478    fn enter_jsx_fragment(&mut self, _elem: &mut JSXFragment<'a>, _ctx: &mut TraverseCtx<'a>) {
479        self.has_jsx_fragment = true;
480    }
481
482    fn enter_formal_parameter_rest(
483        &mut self,
484        node: &mut FormalParameterRest<'a>,
485        _ctx: &mut oxc_traverse::TraverseCtx<'a, TransformState<'a>>,
486    ) {
487        node.type_annotation = None;
488    }
489
490    fn enter_catch_parameter(
491        &mut self,
492        node: &mut CatchParameter<'a>,
493        _ctx: &mut oxc_traverse::TraverseCtx<'a, TransformState<'a>>,
494    ) {
495        node.type_annotation = None;
496    }
497}
498
499impl<'a> TypeScriptAnnotations<'a> {
500    #[inline]
501    fn should_keep_declaration(&self, decl: &Declaration<'a>, ctx: &mut TraverseCtx<'a>) -> bool {
502        match decl {
503            // Remove type aliases, interfaces, and `declare global {}`
504            Declaration::TSTypeAliasDeclaration(_)
505            | Declaration::TSInterfaceDeclaration(_)
506            | Declaration::TSGlobalDeclaration(_) => false,
507            // Remove `declare var/let/const`
508            Declaration::VariableDeclaration(var_decl) => !var_decl.declare,
509            // Remove `declare function` and function overload signatures (no body)
510            Declaration::FunctionDeclaration(func_decl) => {
511                !func_decl.declare && func_decl.body.is_some()
512            }
513            // Remove `declare class`
514            Declaration::ClassDeclaration(class_decl) => !class_decl.declare,
515            // Remove `declare module` or uninstantiated namespace declarations.
516            // Keep instantiated `module` declarations — they have runtime
517            // representation and need to be transformed.
518            Declaration::TSModuleDeclaration(module_decl) => {
519                !module_decl.declare
520                    && !matches!(
521                        &module_decl.id,
522                        TSModuleDeclarationName::Identifier(ident)
523                            if ctx.scoping().symbol_flags(ident.symbol_id()).is_namespace_module()
524                    )
525            }
526            // Remove `declare enum`
527            Declaration::TSEnumDeclaration(enum_decl) => !enum_decl.declare,
528            // Remove unused import-equals (used ones are transformed by module transform)
529            Declaration::TSImportEqualsDeclaration(import_equals) => {
530                let keep = import_equals.import_kind.is_value()
531                    && (self.only_remove_type_imports
532                        || !ctx
533                            .scoping()
534                            .get_resolved_references(import_equals.id.symbol_id())
535                            .all(Reference::is_type));
536                if !keep {
537                    let scope_id = ctx.current_scope_id();
538                    ctx.scoping_mut().remove_binding(scope_id, import_equals.id.name);
539                }
540                keep
541            }
542        }
543    }
544
545    /// Check if the given name is a JSX pragma or fragment pragma import
546    /// and if the file contains JSX elements or fragments
547    fn is_jsx_imports(&self, name: &str) -> bool {
548        self.has_jsx_element && name == self.jsx_element_import_name
549            || self.has_jsx_fragment && name == self.jsx_fragment_import_name
550    }
551
552    fn create_block_with_statement(
553        stmt: Statement<'a>,
554        span: Span,
555        ctx: &mut TraverseCtx<'a>,
556    ) -> Statement<'a> {
557        let scope_id = ctx.insert_scope_below_statement(&stmt, ScopeFlags::empty());
558        ctx.ast.statement_block_with_scope_id(span, ctx.ast.vec1(stmt), scope_id)
559    }
560
561    fn replace_for_statement_body_with_empty_block_if_ts(
562        body: &mut Statement<'a>,
563        parent_scope_id: ScopeId,
564        ctx: &mut TraverseCtx<'a>,
565    ) {
566        Self::replace_with_empty_block_if_ts(body, parent_scope_id, ctx);
567    }
568
569    fn replace_with_empty_block_if_ts(
570        stmt: &mut Statement<'a>,
571        parent_scope_id: ScopeId,
572        ctx: &mut TraverseCtx<'a>,
573    ) {
574        if stmt.is_typescript_syntax() {
575            let scope_id = ctx.create_child_scope(parent_scope_id, ScopeFlags::empty());
576            *stmt = ctx.ast.statement_block_with_scope_id(stmt.span(), ctx.ast.vec(), scope_id);
577        }
578    }
579
580    fn has_value_reference(&self, id: &BindingIdentifier<'a>, ctx: &TraverseCtx<'a>) -> bool {
581        let symbol_id = id.symbol_id();
582
583        // `import T from 'mod'; const T = 1;` The T has a value redeclaration
584        // `import T from 'mod'; type T = number;` The T has a type redeclaration
585        // If the symbol is still a value symbol after `SymbolFlags::Import` is removed, then it's a value redeclaration.
586        // That means the import is shadowed, and we can safely remove the import.
587        if (ctx.scoping().symbol_flags(symbol_id) - SymbolFlags::Import).is_value() {
588            return false;
589        }
590
591        if ctx.scoping().get_resolved_references(symbol_id).any(|reference| !reference.is_type()) {
592            return true;
593        }
594
595        self.is_jsx_imports(&id.name)
596    }
597
598    fn can_retain_export_specifier(specifier: &ExportSpecifier<'a>, ctx: &TraverseCtx<'a>) -> bool {
599        if specifier.export_kind.is_type() {
600            return false;
601        }
602        !matches!(&specifier.local, ModuleExportName::IdentifierReference(ident) if Self::is_refers_to_type(ident, ctx))
603    }
604
605    fn is_refers_to_type(ident: &IdentifierReference<'a>, ctx: &TraverseCtx<'a>) -> bool {
606        let scoping = ctx.scoping();
607        let reference = scoping.get_reference(ident.reference_id());
608
609        reference.symbol_id().is_some_and(|symbol_id| {
610            reference.is_type()
611                || scoping.symbol_flags(symbol_id).is_ambient()
612                    && scoping.symbol_redeclarations(symbol_id).iter().all(|r| r.flags.is_ambient())
613        })
614    }
615}
616
617struct Assignment<'a> {
618    span: Span,
619    name: Str<'a>,
620    symbol_id: SymbolId,
621}
622
623impl<'a> Assignment<'a> {
624    // Creates `this.name = name`
625    fn create_this_property_assignment(&self, ctx: &mut TraverseCtx<'a>) -> Statement<'a> {
626        let reference_id = ctx.create_bound_reference(self.symbol_id, ReferenceFlags::Read);
627        let id = ctx.ast.identifier_reference_with_reference_id(self.span, self.name, reference_id);
628
629        ctx.ast.statement_expression(
630            SPAN,
631            ctx.ast.expression_assignment(
632                SPAN,
633                AssignmentOperator::Assign,
634                SimpleAssignmentTarget::from(ctx.ast.member_expression_static(
635                    SPAN,
636                    ctx.ast.expression_this(SPAN),
637                    ctx.ast.identifier_name(self.span, self.name),
638                    false,
639                ))
640                .into(),
641                Expression::Identifier(ctx.alloc(id)),
642            ),
643        )
644    }
645}