swc_typescript/fast_dts/
mod.rs

1use std::{borrow::Cow, mem::take, sync::Arc};
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use swc_atoms::Atom;
5use swc_common::{
6    comments::SingleThreadedComments, util::take::Take, BytePos, FileName, Mark, Span, Spanned,
7    DUMMY_SP,
8};
9use swc_ecma_ast::{
10    BindingIdent, Decl, DefaultDecl, ExportDefaultExpr, Ident, ImportSpecifier, ModuleDecl,
11    ModuleItem, NamedExport, Pat, Program, Script, Stmt, TsExportAssignment, VarDecl, VarDeclKind,
12    VarDeclarator,
13};
14use type_usage::TypeUsageAnalyzer;
15use util::{
16    ast_ext::MemberPropExt, expando_function_collector::ExpandoFunctionCollector, types::type_ann,
17};
18use visitors::type_usage::{self, SymbolFlags, UsedRefs};
19
20use crate::diagnostic::{DtsIssue, SourceRange};
21
22mod class;
23mod decl;
24mod r#enum;
25mod function;
26mod inferrer;
27mod types;
28mod util;
29mod visitors;
30
31/// TypeScript Isolated Declaration support.
32///
33/// ---
34///
35/// # License
36///
37/// Mostly translated from <https://github.com/oxc-project/oxc/tree/main/crates/oxc_isolated_declarations>
38///
39/// The original code is MIT licensed.
40pub struct FastDts {
41    filename: Arc<FileName>,
42    unresolved_mark: Mark,
43    diagnostics: Vec<DtsIssue>,
44    // states
45    id_counter: u32,
46    is_top_level: bool,
47    used_refs: UsedRefs,
48    internal_annotations: Option<FxHashSet<BytePos>>,
49}
50
51#[derive(Debug, Default)]
52pub struct FastDtsOptions {
53    pub internal_annotations: Option<FxHashSet<BytePos>>,
54}
55
56/// Diagnostics
57impl FastDts {
58    pub fn new(filename: Arc<FileName>, unresolved_mark: Mark, options: FastDtsOptions) -> Self {
59        let internal_annotations = options.internal_annotations;
60        Self {
61            filename,
62            unresolved_mark,
63            diagnostics: Vec::new(),
64            id_counter: 0,
65            is_top_level: true,
66            used_refs: UsedRefs::default(),
67            internal_annotations,
68        }
69    }
70
71    pub fn mark_diagnostic<T: Into<Cow<'static, str>>>(&mut self, message: T, range: Span) {
72        self.diagnostics.push(DtsIssue {
73            message: message.into(),
74            range: SourceRange {
75                filename: self.filename.clone(),
76                span: range,
77            },
78        })
79    }
80}
81
82impl FastDts {
83    pub fn transform(&mut self, program: &mut Program) -> Vec<DtsIssue> {
84        match program {
85            Program::Module(module) => self.transform_module_body(&mut module.body, false),
86            Program::Script(script) => self.transform_script(script),
87        }
88        take(&mut self.diagnostics)
89    }
90
91    fn transform_module_body(
92        &mut self,
93        items: &mut Vec<ModuleItem>,
94        in_global_or_lit_module: bool,
95    ) {
96        // 1. Analyze usage
97        self.used_refs.extend(TypeUsageAnalyzer::analyze(
98            items,
99            self.internal_annotations.as_ref(),
100        ));
101
102        // 2. Transform.
103        Self::remove_function_overloads_in_module(items);
104        self.transform_module_items(items);
105
106        // 3. Strip export keywords in ts module blocks
107        for item in items.iter_mut() {
108            if let Some(Stmt::Decl(Decl::TsModule(ts_module))) = item.as_mut_stmt() {
109                if ts_module.global || !ts_module.id.is_str() {
110                    continue;
111                }
112
113                if let Some(body) = ts_module
114                    .body
115                    .as_mut()
116                    .and_then(|body| body.as_mut_ts_module_block())
117                {
118                    self.strip_export(&mut body.body);
119                }
120            }
121        }
122
123        // 4. Report error for expando function and remove statements.
124        self.report_error_for_expando_function_in_module(items);
125        items.retain(|item| {
126            item.as_stmt()
127                .map(|stmt| stmt.is_decl() && !self.has_internal_annotation(stmt.span_lo()))
128                .unwrap_or(true)
129        });
130
131        // 5. Remove unused imports and decls
132        self.remove_ununsed(items, in_global_or_lit_module);
133
134        // 6. Add empty export mark if there's any declaration that is used but not
135        // exported to keep its privacy.
136        let mut has_non_exported_stmt = false;
137        let mut has_export = false;
138        for item in items.iter_mut() {
139            match item {
140                ModuleItem::Stmt(stmt) => {
141                    if stmt.as_decl().map_or(true, |decl| !decl.is_ts_module()) {
142                        has_non_exported_stmt = true;
143                    }
144                }
145                ModuleItem::ModuleDecl(
146                    ModuleDecl::ExportDefaultDecl(_)
147                    | ModuleDecl::ExportDefaultExpr(_)
148                    | ModuleDecl::ExportNamed(_)
149                    | ModuleDecl::TsExportAssignment(_),
150                ) => has_export = true,
151                _ => {}
152            }
153        }
154        if items.is_empty() || (has_non_exported_stmt && !has_export) {
155            items.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(
156                NamedExport {
157                    span: DUMMY_SP,
158                    specifiers: Vec::new(),
159                    src: None,
160                    type_only: false,
161                    with: None,
162                },
163            )));
164        } else if !self.is_top_level {
165            self.strip_export(items);
166        }
167    }
168
169    fn transform_script(&mut self, script: &mut Script) {
170        // 1. Transform.
171        Self::remove_function_overloads_in_script(script);
172        let body = script.body.take();
173        for mut stmt in body {
174            if self.has_internal_annotation(stmt.span_lo()) {
175                continue;
176            }
177            if let Some(decl) = stmt.as_mut_decl() {
178                self.transform_decl(decl, false);
179            }
180            script.body.push(stmt);
181        }
182
183        // 2. Report error for expando function and remove statements.
184        self.report_error_for_expando_function_in_script(&script.body);
185        script
186            .body
187            .retain(|stmt| stmt.is_decl() && !self.has_internal_annotation(stmt.span_lo()));
188    }
189
190    fn transform_module_items(&mut self, items: &mut Vec<ModuleItem>) {
191        let orig_items = take(items);
192
193        for mut item in orig_items {
194            match &mut item {
195                ModuleItem::ModuleDecl(
196                    ModuleDecl::Import(..)
197                    | ModuleDecl::TsImportEquals(_)
198                    | ModuleDecl::TsNamespaceExport(_),
199                ) => items.push(item),
200                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(_) | ModuleDecl::ExportAll(_)) => {
201                    items.push(item);
202                }
203                ModuleItem::Stmt(stmt) => {
204                    if self.has_internal_annotation(stmt.span_lo()) {
205                        continue;
206                    }
207
208                    if let Some(decl) = stmt.as_mut_decl() {
209                        self.transform_decl(decl, true);
210                    }
211                    items.push(item);
212                }
213                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(expor_decl)) => {
214                    if self.has_internal_annotation(expor_decl.span_lo()) {
215                        continue;
216                    }
217                    self.transform_decl(&mut expor_decl.decl, false);
218                    items.push(item);
219                }
220                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export)) => {
221                    self.transform_default_decl(&mut export.decl);
222                    items.push(item);
223                }
224                ModuleItem::ModuleDecl(
225                    ModuleDecl::ExportDefaultExpr(_) | ModuleDecl::TsExportAssignment(_),
226                ) => {
227                    let expr = match &item {
228                        ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) => {
229                            &export.expr
230                        }
231                        ModuleItem::ModuleDecl(ModuleDecl::TsExportAssignment(export)) => {
232                            &export.expr
233                        }
234                        _ => unreachable!(),
235                    };
236
237                    if expr.is_ident() {
238                        items.push(item);
239                        continue;
240                    }
241
242                    let name_ident = Ident::new_no_ctxt(self.gen_unique_name("_default"), DUMMY_SP);
243                    let type_ann = self.infer_type_from_expr(expr).map(type_ann);
244                    self.used_refs
245                        .add_usage(name_ident.to_id(), SymbolFlags::Value);
246
247                    if type_ann.is_none() {
248                        self.default_export_inferred(expr.span());
249                    }
250
251                    items.push(
252                        VarDecl {
253                            span: DUMMY_SP,
254                            kind: VarDeclKind::Const,
255                            declare: true,
256                            decls: vec![VarDeclarator {
257                                span: DUMMY_SP,
258                                name: Pat::Ident(BindingIdent {
259                                    id: name_ident.clone(),
260                                    type_ann,
261                                }),
262                                init: None,
263                                definite: false,
264                            }],
265                            ..Default::default()
266                        }
267                        .into(),
268                    );
269
270                    match &item {
271                        ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) => items
272                            .push(
273                                ExportDefaultExpr {
274                                    span: export.span,
275                                    expr: name_ident.into(),
276                                }
277                                .into(),
278                            ),
279                        ModuleItem::ModuleDecl(ModuleDecl::TsExportAssignment(export)) => items
280                            .push(
281                                TsExportAssignment {
282                                    span: export.span,
283                                    expr: name_ident.into(),
284                                }
285                                .into(),
286                            ),
287                        _ => unreachable!(),
288                    };
289                }
290            }
291        }
292    }
293
294    fn report_error_for_expando_function_in_module(&mut self, items: &[ModuleItem]) {
295        let used_refs = self.used_refs.clone();
296        let mut assignable_properties_for_namespace = FxHashMap::<&str, FxHashSet<Atom>>::default();
297        let mut collector = ExpandoFunctionCollector::new(&used_refs);
298
299        for item in items {
300            let decl = match item {
301                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
302                    if let Some(ts_module) = export_decl.decl.as_ts_module() {
303                        ts_module
304                    } else {
305                        continue;
306                    }
307                }
308                ModuleItem::Stmt(Stmt::Decl(Decl::TsModule(ts_module))) => ts_module,
309                _ => continue,
310            };
311
312            let (Some(name), Some(block)) = (
313                decl.id.as_ident(),
314                decl.body
315                    .as_ref()
316                    .and_then(|body| body.as_ts_module_block()),
317            ) else {
318                continue;
319            };
320
321            for item in &block.body {
322                // Note that all the module blocks have been transformed
323                let Some(decl) = item.as_stmt().and_then(|stmt| stmt.as_decl()) else {
324                    continue;
325                };
326
327                match &decl {
328                    Decl::Class(class_decl) => {
329                        assignable_properties_for_namespace
330                            .entry(name.sym.as_str())
331                            .or_default()
332                            .insert(class_decl.ident.sym.clone());
333                    }
334                    Decl::Fn(fn_decl) => {
335                        assignable_properties_for_namespace
336                            .entry(name.sym.as_str())
337                            .or_default()
338                            .insert(fn_decl.ident.sym.clone());
339                    }
340                    Decl::Var(var_decl) => {
341                        for decl in &var_decl.decls {
342                            if let Some(ident) = decl.name.as_ident() {
343                                assignable_properties_for_namespace
344                                    .entry(name.sym.as_str())
345                                    .or_default()
346                                    .insert(ident.sym.clone());
347                            }
348                        }
349                    }
350                    Decl::Using(using_decl) => {
351                        for decl in &using_decl.decls {
352                            if let Some(ident) = decl.name.as_ident() {
353                                assignable_properties_for_namespace
354                                    .entry(name.sym.as_str())
355                                    .or_default()
356                                    .insert(ident.sym.clone());
357                            }
358                        }
359                    }
360                    _ => {}
361                }
362            }
363        }
364
365        for item in items {
366            match item {
367                ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) => {
368                    match &export_decl.decl {
369                        Decl::Fn(fn_decl) => collector.add_fn_decl(fn_decl, false),
370                        Decl::Var(var_decl) => collector.add_var_decl(var_decl, false),
371                        _ => (),
372                    }
373                }
374                ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(export_decl)) => {
375                    if let DefaultDecl::Fn(fn_expr) = &export_decl.decl {
376                        collector.add_fn_expr(fn_expr)
377                    }
378                }
379                ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(_export_named)) => {
380                    // TODO: may be function
381                }
382                ModuleItem::Stmt(Stmt::Decl(decl)) => match decl {
383                    Decl::Fn(fn_decl) => collector.add_fn_decl(fn_decl, true),
384                    Decl::Var(var_decl) => collector.add_var_decl(var_decl, true),
385                    _ => (),
386                },
387                ModuleItem::Stmt(Stmt::Expr(expr_stmt)) => {
388                    let Some(assign_expr) = expr_stmt.expr.as_assign() else {
389                        continue;
390                    };
391                    let Some(member_expr) = assign_expr
392                        .left
393                        .as_simple()
394                        .and_then(|simple| simple.as_member())
395                    else {
396                        continue;
397                    };
398
399                    if let Some(ident) = member_expr.obj.as_ident() {
400                        if collector.contains(&ident.sym)
401                            && !assignable_properties_for_namespace
402                                .get(ident.sym.as_str())
403                                .is_some_and(|properties| {
404                                    member_expr
405                                        .prop
406                                        .static_name()
407                                        .is_some_and(|name| properties.contains(name))
408                                })
409                        {
410                            self.function_with_assigning_properties(member_expr.span);
411                        }
412                    }
413                }
414                _ => (),
415            }
416        }
417    }
418
419    fn report_error_for_expando_function_in_script(&mut self, stmts: &[Stmt]) {
420        let used_refs = self.used_refs.clone();
421        let mut collector = ExpandoFunctionCollector::new(&used_refs);
422        for stmt in stmts {
423            match stmt {
424                Stmt::Decl(decl) => match decl {
425                    Decl::Fn(fn_decl) => collector.add_fn_decl(fn_decl, false),
426                    Decl::Var(var_decl) => collector.add_var_decl(var_decl, false),
427                    _ => (),
428                },
429                Stmt::Expr(expr_stmt) => {
430                    let Some(assign_expr) = expr_stmt.expr.as_assign() else {
431                        continue;
432                    };
433                    let Some(member_expr) = assign_expr
434                        .left
435                        .as_simple()
436                        .and_then(|simple| simple.as_member())
437                    else {
438                        continue;
439                    };
440
441                    if let Some(ident) = member_expr.obj.as_ident() {
442                        if collector.contains(&ident.sym) {
443                            self.function_with_assigning_properties(member_expr.span);
444                        }
445                    }
446                }
447                _ => (),
448            }
449        }
450    }
451
452    fn strip_export(&self, items: &mut Vec<ModuleItem>) {
453        for item in items {
454            if let ModuleItem::ModuleDecl(ModuleDecl::ExportDecl(export_decl)) = item {
455                *item = ModuleItem::Stmt(Stmt::Decl(export_decl.decl.clone()));
456            }
457        }
458    }
459
460    fn remove_ununsed(&self, items: &mut Vec<ModuleItem>, in_global_or_lit_module: bool) {
461        let used_refs = &self.used_refs;
462        items.retain_mut(|node| match node {
463            ModuleItem::Stmt(Stmt::Decl(decl)) if !in_global_or_lit_module => match decl {
464                Decl::Class(class_decl) => used_refs.used(&class_decl.ident.to_id()),
465                Decl::Fn(fn_decl) => used_refs.used_as_value(&fn_decl.ident.to_id()),
466                Decl::Var(var_decl) => {
467                    var_decl.decls.retain(|decl| {
468                        if let Some(ident) = decl.name.as_ident() {
469                            used_refs.used_as_value(&ident.to_id())
470                        } else {
471                            false
472                        }
473                    });
474                    !var_decl.decls.is_empty()
475                }
476                Decl::Using(using_decl) => {
477                    using_decl.decls.retain(|decl| {
478                        if let Some(ident) = decl.name.as_ident() {
479                            used_refs.used_as_value(&ident.to_id())
480                        } else {
481                            false
482                        }
483                    });
484                    !using_decl.decls.is_empty()
485                }
486                Decl::TsInterface(ts_interface_decl) => {
487                    used_refs.used_as_type(&ts_interface_decl.id.to_id())
488                }
489                Decl::TsTypeAlias(ts_type_alias_decl) => {
490                    used_refs.used_as_type(&ts_type_alias_decl.id.to_id())
491                }
492                Decl::TsEnum(ts_enum) => used_refs.used(&ts_enum.id.to_id()),
493                Decl::TsModule(ts_module_decl) => {
494                    ts_module_decl.global
495                        || ts_module_decl.id.is_str()
496                        || ts_module_decl
497                            .id
498                            .as_ident()
499                            .map_or(true, |ident| used_refs.used_as_type(&ident.to_id()))
500                }
501            },
502            ModuleItem::ModuleDecl(ModuleDecl::Import(import_decl)) => {
503                if import_decl.specifiers.is_empty() {
504                    return true;
505                }
506
507                import_decl.specifiers.retain(|specifier| match specifier {
508                    ImportSpecifier::Named(specifier) => used_refs.used(&specifier.local.to_id()),
509                    ImportSpecifier::Default(specifier) => used_refs.used(&specifier.local.to_id()),
510                    ImportSpecifier::Namespace(specifier) => {
511                        used_refs.used(&specifier.local.to_id())
512                    }
513                });
514
515                !import_decl.specifiers.is_empty()
516            }
517            ModuleItem::ModuleDecl(ModuleDecl::TsImportEquals(ts_import_equals)) => {
518                used_refs.used(&ts_import_equals.id.to_id())
519            }
520            _ => true,
521        });
522    }
523
524    pub fn has_internal_annotation(&self, pos: BytePos) -> bool {
525        if let Some(internal_annotations) = &self.internal_annotations {
526            return internal_annotations.contains(&pos);
527        }
528        false
529    }
530
531    pub fn get_internal_annotations(comments: &SingleThreadedComments) -> FxHashSet<BytePos> {
532        let mut internal_annotations = FxHashSet::default();
533        let (leading, _) = comments.borrow_all();
534        for (pos, comment) in leading.iter() {
535            let has_internal_annotation = comment
536                .iter()
537                .any(|comment| comment.text.contains("@internal"));
538            if has_internal_annotation {
539                internal_annotations.insert(*pos);
540            }
541        }
542        internal_annotations
543    }
544
545    fn gen_unique_name(&mut self, name: &str) -> Atom {
546        self.id_counter += 1;
547        format!("{name}_{}", self.id_counter).into()
548    }
549}