Skip to main content

ts_gen/parse/first_pass/
converters.rs

1//! Individual declaration converters: AST → IR.
2//!
3//! Pure functions that convert oxc AST nodes into IR declarations.
4//! Used by both Phase 1 (for classification) and Phase 2 (for full population).
5
6use oxc_ast::ast;
7
8use crate::ir;
9use crate::parse::classify::classify_interface;
10use crate::parse::docs::DocComments;
11use crate::parse::members::{convert_class_element, convert_ts_signature};
12use crate::parse::types::{convert_formal_params, convert_type_params};
13use crate::util::diagnostics::DiagnosticCollector;
14use crate::util::naming::to_snake_case;
15
16pub fn convert_class_decl(
17    class: &ast::Class<'_>,
18    ctx: &ir::ModuleContext,
19    docs: &DocComments<'_>,
20    diag: &mut DiagnosticCollector,
21) -> Option<ir::ClassDecl> {
22    let name = class.id.as_ref()?.name.to_string();
23    let js_name = name.clone();
24
25    let type_params = convert_type_params(class.type_parameters.as_ref(), diag);
26
27    let extends = class
28        .super_class
29        .as_ref()
30        .and_then(|sc| match expression_to_dotted_name(sc) {
31            Some(name) => Some(ir::TypeRef::Named(name)),
32            None => {
33                diag.warn("Complex super class expression is not supported");
34                None
35            }
36        });
37
38    let implements: Vec<ir::TypeRef> = class
39        .implements
40        .iter()
41        .map(|i| convert_ts_type_name_to_ref(&i.expression))
42        .collect();
43
44    let is_abstract = class.r#abstract;
45
46    let members: Vec<ir::Member> = class
47        .body
48        .body
49        .iter()
50        .flat_map(|elem| convert_class_element(elem, docs, diag))
51        .collect();
52
53    Some(ir::ClassDecl {
54        name,
55        js_name,
56        type_params,
57        extends,
58        implements,
59        is_abstract,
60        members,
61        type_module_context: ctx.clone(),
62    })
63}
64
65pub fn convert_interface_decl(
66    iface: &ast::TSInterfaceDeclaration<'_>,
67    docs: &DocComments<'_>,
68    diag: &mut DiagnosticCollector,
69) -> ir::InterfaceDecl {
70    let name = iface.id.name.to_string();
71    let js_name = name.clone();
72
73    let type_params = convert_type_params(iface.type_parameters.as_ref(), diag);
74
75    let extends: Vec<ir::TypeRef> = iface
76        .extends
77        .iter()
78        .map(|ext| convert_ts_type_from_heritage(&ext.expression, diag))
79        .collect();
80
81    let members: Vec<ir::Member> = iface
82        .body
83        .body
84        .iter()
85        .flat_map(|sig| convert_ts_signature(sig, docs, diag))
86        .collect();
87
88    let classification = classify_interface(&members);
89
90    ir::InterfaceDecl {
91        name,
92        js_name,
93        type_params,
94        extends,
95        members,
96        classification,
97    }
98}
99
100pub fn convert_function_decl(
101    func: &ast::Function<'_>,
102    diag: &mut DiagnosticCollector,
103) -> Option<ir::FunctionDecl> {
104    let name = func.id.as_ref()?.name.to_string();
105    let js_name = name.clone();
106    let rust_name = to_snake_case(&name);
107
108    let type_params = convert_type_params(func.type_parameters.as_ref(), diag);
109
110    // Build scope from function type parameters so generic references get erased
111    let scope: std::collections::HashSet<&str> = func
112        .type_parameters
113        .as_ref()
114        .map(|tp| tp.params.iter().map(|p| p.name.name.as_str()).collect())
115        .unwrap_or_default();
116
117    let params = convert_formal_params(&func.params, diag);
118    let return_type = func
119        .return_type
120        .as_ref()
121        .map(|rt| crate::parse::types::convert_ts_type_scoped(&rt.type_annotation, &scope, diag))
122        .unwrap_or(ir::TypeRef::Void);
123
124    Some(ir::FunctionDecl {
125        name: rust_name,
126        js_name,
127        type_params,
128        params,
129        return_type,
130        overloads: vec![],
131    })
132}
133
134pub fn convert_string_enum(name: &str, ts_type: &ast::TSType<'_>) -> Option<ir::StringEnumDecl> {
135    let variants = match ts_type {
136        ast::TSType::TSUnionType(union) => union
137            .types
138            .iter()
139            .filter_map(|t| {
140                if let ast::TSType::TSLiteralType(lit) = t {
141                    if let ast::TSLiteral::StringLiteral(s) = &lit.literal {
142                        let js_value = s.value.to_string();
143                        let rust_name = crate::util::naming::to_enum_variant(&js_value);
144                        return Some(ir::StringEnumVariant {
145                            rust_name,
146                            js_value,
147                        });
148                    }
149                }
150                None
151            })
152            .collect(),
153        ast::TSType::TSLiteralType(lit) => {
154            if let ast::TSLiteral::StringLiteral(s) = &lit.literal {
155                let js_value = s.value.to_string();
156                let rust_name = crate::util::naming::to_enum_variant(&js_value);
157                vec![ir::StringEnumVariant {
158                    rust_name,
159                    js_value,
160                }]
161            } else {
162                return None;
163            }
164        }
165        _ => return None,
166    };
167
168    // Deduplicate variant names (e.g., "text-plain" and "textPlain" both → "TextPlain")
169    let mut rust_names: Vec<String> = variants.iter().map(|v| v.rust_name.clone()).collect();
170    crate::util::naming::dedup_names(&mut rust_names);
171    let variants: Vec<_> = variants
172        .into_iter()
173        .zip(rust_names)
174        .map(|(mut v, name)| {
175            v.rust_name = name;
176            v
177        })
178        .collect();
179
180    Some(ir::StringEnumDecl {
181        name: name.to_string(),
182        variants,
183    })
184}
185
186/// Classify a TS enum as string or numeric based on its member initializers.
187pub fn classify_ts_enum_kind(enum_decl: &ast::TSEnumDeclaration<'_>) -> ir::RegisteredKind {
188    let mut has_string = false;
189    let mut has_numeric = false;
190
191    for member in &enum_decl.body.members {
192        match &member.initializer {
193            Some(ast::Expression::StringLiteral(_)) => has_string = true,
194            Some(ast::Expression::NumericLiteral(_)) => has_numeric = true,
195            Some(ast::Expression::UnaryExpression(_)) => has_numeric = true,
196            None => {
197                has_numeric = true;
198            }
199            _ => {}
200        }
201    }
202
203    if has_string && !has_numeric {
204        ir::RegisteredKind::StringEnum
205    } else if has_numeric {
206        ir::RegisteredKind::NumericEnum
207    } else {
208        ir::RegisteredKind::StringEnum
209    }
210}
211
212/// Convert a TS enum with string values to our StringEnumDecl IR.
213pub fn convert_string_ts_enum(enum_decl: &ast::TSEnumDeclaration<'_>) -> ir::StringEnumDecl {
214    let name = enum_decl.id.name.to_string();
215    let variants: Vec<_> = enum_decl
216        .body
217        .members
218        .iter()
219        .filter_map(|member| {
220            let member_name = match &member.id {
221                ast::TSEnumMemberName::Identifier(id) => id.name.to_string(),
222                ast::TSEnumMemberName::String(s) => s.value.to_string(),
223                _ => return None,
224            };
225            let js_value = match &member.initializer {
226                Some(ast::Expression::StringLiteral(s)) => s.value.to_string(),
227                _ => member_name.clone(),
228            };
229            let rust_name = crate::util::naming::to_enum_variant(&member_name);
230            Some(ir::StringEnumVariant {
231                rust_name,
232                js_value,
233            })
234        })
235        .collect();
236
237    let mut rust_names: Vec<String> = variants.iter().map(|v| v.rust_name.clone()).collect();
238    crate::util::naming::dedup_names(&mut rust_names);
239    let variants: Vec<_> = variants
240        .into_iter()
241        .zip(rust_names)
242        .map(|(mut v, name)| {
243            v.rust_name = name;
244            v
245        })
246        .collect();
247
248    ir::StringEnumDecl { name, variants }
249}
250
251/// Convert a TS enum with numeric values to our NumericEnumDecl IR.
252pub fn convert_numeric_enum(
253    enum_decl: &ast::TSEnumDeclaration<'_>,
254    docs: &DocComments<'_>,
255    diag: &mut DiagnosticCollector,
256) -> ir::NumericEnumDecl {
257    let name = enum_decl.id.name.to_string();
258    let mut next_value: i64 = 0;
259
260    let variants: Vec<_> = enum_decl
261        .body
262        .members
263        .iter()
264        .filter_map(|member| {
265            let member_name = match &member.id {
266                ast::TSEnumMemberName::Identifier(id) => id.name.to_string(),
267                ast::TSEnumMemberName::String(s) => s.value.to_string(),
268                _ => return None,
269            };
270
271            let value = match &member.initializer {
272                Some(ast::Expression::NumericLiteral(n)) => {
273                    let v = f64_to_i64(n.value, &member_name, &name, diag);
274                    next_value = v + 1;
275                    v
276                }
277                Some(ast::Expression::UnaryExpression(unary)) => {
278                    if let ast::Expression::NumericLiteral(n) = &unary.argument {
279                        let raw = f64_to_i64(n.value, &member_name, &name, diag);
280                        let v = match unary.operator.as_str() {
281                            "-" => -raw,
282                            "~" => !raw,
283                            _ => raw,
284                        };
285                        next_value = v + 1;
286                        v
287                    } else {
288                        let v = next_value;
289                        next_value += 1;
290                        v
291                    }
292                }
293                None => {
294                    let v = next_value;
295                    next_value += 1;
296                    v
297                }
298                _ => {
299                    let v = next_value;
300                    next_value += 1;
301                    v
302                }
303            };
304
305            let doc = docs.for_span(member.span.start);
306            let rust_name = crate::util::naming::to_enum_variant(&member_name);
307
308            Some(ir::NumericEnumVariant {
309                rust_name,
310                js_name: member_name,
311                value,
312                doc,
313            })
314        })
315        .collect();
316
317    let mut rust_names: Vec<String> = variants.iter().map(|v| v.rust_name.clone()).collect();
318    crate::util::naming::dedup_names(&mut rust_names);
319    let variants: Vec<_> = variants
320        .into_iter()
321        .zip(rust_names)
322        .map(|(mut v, name)| {
323            v.rust_name = name;
324            v
325        })
326        .collect();
327
328    ir::NumericEnumDecl { name, variants }
329}
330
331fn convert_ts_type_from_heritage(
332    expr: &ast::Expression<'_>,
333    diag: &mut DiagnosticCollector,
334) -> ir::TypeRef {
335    match expression_to_dotted_name(expr) {
336        Some(name) => ir::TypeRef::Named(name),
337        None => {
338            diag.warn("Unsupported heritage expression, falling back to Object");
339            ir::TypeRef::Named("Object".to_string())
340        }
341    }
342}
343
344/// Extract a dotted name from an expression (e.g., `Foo.Bar.Baz` → `"Foo.Bar.Baz"`).
345pub fn expression_to_dotted_name(expr: &ast::Expression<'_>) -> Option<String> {
346    match expr {
347        ast::Expression::Identifier(ident) => Some(ident.name.to_string()),
348        ast::Expression::StaticMemberExpression(member) => {
349            let left = expression_to_dotted_name(&member.object)?;
350            Some(format!("{left}.{}", member.property.name))
351        }
352        _ => None,
353    }
354}
355
356fn convert_ts_type_name_to_ref(type_name: &ast::TSTypeName<'_>) -> ir::TypeRef {
357    match type_name {
358        ast::TSTypeName::IdentifierReference(ident) => ir::TypeRef::Named(ident.name.to_string()),
359        ast::TSTypeName::QualifiedName(qualified) => {
360            let left = convert_ts_type_name_to_string(&qualified.left);
361            let right = &qualified.right.name;
362            ir::TypeRef::Named(format!("{left}.{right}"))
363        }
364        ast::TSTypeName::ThisExpression(_) => ir::TypeRef::Unresolved("this".to_string()),
365    }
366}
367
368/// Debug name for an `ExportDefaultDeclarationKind` variant.
369pub fn export_default_kind_name(kind: &ast::ExportDefaultDeclarationKind<'_>) -> &'static str {
370    match kind {
371        ast::ExportDefaultDeclarationKind::ClassDeclaration(_) => "ClassDeclaration",
372        ast::ExportDefaultDeclarationKind::FunctionDeclaration(_) => "FunctionDeclaration",
373        ast::ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => "TSInterfaceDeclaration",
374        _ => "Expression",
375    }
376}
377
378/// Convert f64 (JS number) to i64 with a diagnostic if the value is not
379/// an exact integer or is out of i64 range.
380fn f64_to_i64(
381    value: f64,
382    member_name: &str,
383    enum_name: &str,
384    diag: &mut DiagnosticCollector,
385) -> i64 {
386    if value.fract() != 0.0 {
387        diag.warn(format!(
388            "Enum `{enum_name}::{member_name}` has non-integer value {value}, truncating to {}",
389            value as i64
390        ));
391    } else if value > i64::MAX as f64 || value < i64::MIN as f64 {
392        diag.warn(format!(
393            "Enum `{enum_name}::{member_name}` value {value} is out of i64 range, truncating"
394        ));
395    }
396    let result = value as i64;
397    // Warn if value won't fit in the codegen repr (i32 for signed, u32 for unsigned)
398    if i32::try_from(result).is_err() && u32::try_from(result).is_err() {
399        diag.warn(format!(
400            "Enum `{enum_name}::{member_name}` value {result} exceeds i32/u32 range, \
401             will be truncated in generated code"
402        ));
403    }
404    result
405}
406
407fn convert_ts_type_name_to_string(type_name: &ast::TSTypeName<'_>) -> String {
408    match type_name {
409        ast::TSTypeName::IdentifierReference(ident) => ident.name.to_string(),
410        ast::TSTypeName::QualifiedName(qualified) => {
411            let left = convert_ts_type_name_to_string(&qualified.left);
412            let right = &qualified.right.name;
413            format!("{left}.{right}")
414        }
415        ast::TSTypeName::ThisExpression(_) => "this".to_string(),
416    }
417}