Skip to main content

ts_gen/parse/first_pass/
mod.rs

1//! Two-phase first pass over the oxc AST to build the IR.
2//!
3//! - Phase 1 (`collect`): Collect all type names into the `TypeRegistry`.
4//! - Phase 2 (`populate`): Fully populate IR declarations using the registry for resolution.
5//! - `converters`: Pure AST → IR conversion functions shared by both phases.
6
7use oxc_ast::ast;
8
9use crate::ir;
10use crate::parse::scope::ScopeId;
11
12mod collect;
13pub mod converters;
14mod populate;
15
16// Re-export public entry points.
17pub use collect::collect_type_names;
18pub use populate::populate_declarations;
19
20// ─── Script vs Module Detection ─────────────────────────────────────
21
22/// Detect whether a file is a TypeScript "script" or "module".
23///
24/// A file is a **module** if any top-level statement is an `import` or `export`.
25/// Script files have all top-level declarations in global scope.
26/// Module files have declarations local to the file — only `declare global {}`
27/// blocks affect the global scope.
28pub fn is_module(program: &ast::Program<'_>) -> bool {
29    program.body.iter().any(|stmt| {
30        matches!(
31            stmt,
32            ast::Statement::ImportDeclaration(_)
33                | ast::Statement::ExportAllDeclaration(_)
34                | ast::Statement::ExportDefaultDeclaration(_)
35                | ast::Statement::ExportNamedDeclaration(_)
36                | ast::Statement::TSExportAssignment(_)
37        )
38    })
39}
40
41// ─── Shared Helpers ─────────────────────────────────────────────────
42
43fn is_string_literal_union(ts_type: &ast::TSType<'_>) -> bool {
44    match ts_type {
45        ast::TSType::TSUnionType(union) => union.types.iter().all(|t| {
46            matches!(
47                t,
48                ast::TSType::TSLiteralType(lit) if matches!(&lit.literal, ast::TSLiteral::StringLiteral(_))
49            )
50        }),
51        ast::TSType::TSLiteralType(lit) => matches!(&lit.literal, ast::TSLiteral::StringLiteral(_)),
52        _ => false,
53    }
54}
55
56fn register_type(
57    name: &str,
58    kind: ir::RegisteredKind,
59    ctx: &ir::ModuleContext,
60    registry: &mut ir::TypeRegistry,
61    gctx: &mut crate::context::GlobalContext,
62    scope: ScopeId,
63    exported: bool,
64) {
65    registry.types.insert(
66        name.to_string(),
67        ir::TypeInfo {
68            kind: kind.clone(),
69            primary_context: ctx.clone(),
70        },
71    );
72
73    // Create a placeholder TypeDeclaration based on the registered kind.
74    let name_str = name.to_string();
75    let placeholder_kind = match kind {
76        ir::RegisteredKind::Class | ir::RegisteredKind::MergedClassLike => {
77            ir::TypeKind::Class(ir::ClassDecl {
78                name: name_str.clone(),
79                js_name: name_str.clone(),
80                type_params: vec![],
81                extends: None,
82                implements: vec![],
83                is_abstract: false,
84                members: vec![],
85                type_module_context: ctx.clone(),
86            })
87        }
88        ir::RegisteredKind::Interface => ir::TypeKind::Interface(ir::InterfaceDecl {
89            name: name_str.clone(),
90            js_name: name_str.clone(),
91            type_params: vec![],
92            extends: vec![],
93            members: vec![],
94            classification: ir::InterfaceClassification::Unclassified,
95        }),
96        ir::RegisteredKind::StringEnum => ir::TypeKind::StringEnum(ir::StringEnumDecl {
97            name: name_str.clone(),
98            variants: vec![],
99        }),
100        ir::RegisteredKind::NumericEnum => ir::TypeKind::NumericEnum(ir::NumericEnumDecl {
101            name: name_str.clone(),
102            variants: vec![],
103        }),
104        ir::RegisteredKind::TypeAlias => ir::TypeKind::TypeAlias(ir::TypeAliasDecl {
105            name: name_str.clone(),
106            type_params: vec![],
107            target: ir::TypeRef::Any,
108            from_module: None,
109        }),
110        ir::RegisteredKind::Function => ir::TypeKind::Function(ir::FunctionDecl {
111            name: name_str.clone(),
112            js_name: name_str.clone(),
113            type_params: vec![],
114            params: vec![],
115            return_type: ir::TypeRef::Any,
116            overloads: vec![],
117        }),
118        ir::RegisteredKind::Variable => ir::TypeKind::Variable(ir::VariableDecl {
119            name: name_str.clone(),
120            js_name: name_str.clone(),
121            type_ref: ir::TypeRef::Any,
122            is_const: false,
123        }),
124        ir::RegisteredKind::Namespace => {
125            let ns_scope = gctx.scopes.create_child(scope);
126            ir::TypeKind::Namespace(ir::NamespaceDecl {
127                name: name_str.clone(),
128                declarations: vec![],
129                child_scope: ns_scope,
130            })
131        }
132    };
133
134    let type_id = gctx.insert_type(ir::TypeDeclaration {
135        kind: placeholder_kind,
136        module_context: ctx.clone(),
137        doc: None,
138        scope_id: scope,
139        exported,
140    });
141    gctx.scopes.insert(scope, name_str, type_id);
142}
143
144/// Register an import as a pending import.
145fn register_import(
146    local_name: &str,
147    original_name: &str,
148    from_module: &str,
149    gctx: &mut crate::context::GlobalContext,
150    scope: ScopeId,
151) {
152    gctx.pending_imports
153        .push(crate::parse::scope::PendingImport {
154            scope,
155            local_name: local_name.to_string(),
156            from_module: from_module.to_string(),
157            original_name: original_name.to_string(),
158        });
159}