Skip to main content

ts_gen/parse/
types.rs

1//! Convert oxc TypeScript type AST nodes to our IR `TypeRef`.
2
3use std::collections::HashSet;
4
5use oxc_ast::ast::*;
6
7use crate::ir::TypeRef;
8use crate::util::diagnostics::DiagnosticCollector;
9use crate::util::naming::to_snake_case;
10
11/// Set of in-scope generic type parameter names.
12/// Names in this set resolve to `TypeRef::Any` instead of `TypeRef::Named`.
13pub type TypeParamScope<'a> = HashSet<&'a str>;
14
15/// Convert an oxc `TSType` to our IR `TypeRef`.
16///
17/// If the type appears inside a generic declaration (class, method, function),
18/// pass the in-scope type parameter names via `convert_ts_type_scoped` instead.
19pub fn convert_ts_type(ts_type: &TSType<'_>, diag: &mut DiagnosticCollector) -> TypeRef {
20    convert_ts_type_scoped(ts_type, &HashSet::new(), diag)
21}
22
23/// Convert an oxc `TSType` to our IR `TypeRef`, with type parameter scope.
24///
25/// Type parameters in `scope` are erased to `TypeRef::Any` since they can't
26/// be represented at the wasm_bindgen FFI boundary.
27pub fn convert_ts_type_scoped(
28    ts_type: &TSType<'_>,
29    scope: &TypeParamScope<'_>,
30    diag: &mut DiagnosticCollector,
31) -> TypeRef {
32    // Unwrap parenthesized types
33    let ts_type = ts_type.without_parenthesized();
34
35    match ts_type {
36        // === Keyword types ===
37        TSType::TSAnyKeyword(_) => TypeRef::Any,
38        TSType::TSBooleanKeyword(_) => TypeRef::Boolean,
39        TSType::TSBigIntKeyword(_) => TypeRef::BigInt,
40        TSType::TSNeverKeyword(_) => TypeRef::Any, // erase `never` to `any`
41        TSType::TSNullKeyword(_) => TypeRef::Null,
42        TSType::TSNumberKeyword(_) => TypeRef::Number,
43        TSType::TSObjectKeyword(_) => TypeRef::Object,
44        TSType::TSStringKeyword(_) => TypeRef::String,
45        TSType::TSSymbolKeyword(_) => TypeRef::Symbol,
46        TSType::TSUndefinedKeyword(_) => TypeRef::Undefined,
47        TSType::TSUnknownKeyword(_) => TypeRef::Unknown,
48        TSType::TSVoidKeyword(_) => TypeRef::Void,
49        TSType::TSIntrinsicKeyword(_) => TypeRef::Any,
50
51        // === Type reference (named type, generic instantiation) ===
52        TSType::TSTypeReference(type_ref) => convert_type_reference_scoped(type_ref, scope, diag),
53
54        // === Array types ===
55        TSType::TSArrayType(arr) => {
56            let inner = convert_ts_type_scoped(&arr.element_type, scope, diag);
57            TypeRef::Array(Box::new(inner))
58        }
59
60        // === Union types ===
61        TSType::TSUnionType(union_type) => {
62            let types: Vec<TypeRef> = union_type
63                .types
64                .iter()
65                .map(|t| convert_ts_type_scoped(t, scope, diag))
66                .collect();
67            simplify_union(types)
68        }
69
70        // === Intersection types ===
71        TSType::TSIntersectionType(inter) => {
72            let types: Vec<TypeRef> = inter
73                .types
74                .iter()
75                .map(|t| convert_ts_type_scoped(t, scope, diag))
76                .collect();
77            TypeRef::Intersection(types)
78        }
79
80        // === Tuple types ===
81        TSType::TSTupleType(tuple) => {
82            let types: Vec<TypeRef> = tuple
83                .element_types
84                .iter()
85                .map(|elem| convert_tuple_element_scoped(elem, scope, diag))
86                .collect();
87            TypeRef::Tuple(types)
88        }
89
90        // === Function types ===
91        TSType::TSFunctionType(func) => {
92            let sig = convert_function_type_scoped(func, scope, diag);
93            TypeRef::Function(sig)
94        }
95
96        // === Literal types ===
97        TSType::TSLiteralType(lit) => convert_literal_type(lit, diag),
98
99        // === Type literal (object type) ===
100        TSType::TSTypeLiteral(_) => TypeRef::Object,
101
102        // === Constructor type ===
103        TSType::TSConstructorType(_) => {
104            diag.warn("Constructor types are not supported, erasing to JsValue");
105            TypeRef::Any
106        }
107
108        // === Conditional types, mapped types, template literals, etc. ===
109        TSType::TSConditionalType(_) => {
110            diag.warn("Conditional types are not supported, erasing to JsValue");
111            TypeRef::Unresolved("conditional type".to_string())
112        }
113        TSType::TSMappedType(_) => {
114            diag.warn("Mapped types are not supported, erasing to JsValue");
115            TypeRef::Unresolved("mapped type".to_string())
116        }
117        TSType::TSTemplateLiteralType(_) => TypeRef::String,
118        TSType::TSIndexedAccessType(_) => {
119            diag.warn("Indexed access types are not supported, erasing to JsValue");
120            TypeRef::Unresolved("indexed access type".to_string())
121        }
122        TSType::TSInferType(_) => {
123            diag.warn("Infer types are not supported, erasing to JsValue");
124            TypeRef::Unresolved("infer type".to_string())
125        }
126        TSType::TSTypeOperatorType(op) => match op.operator {
127            TSTypeOperatorOperator::Readonly => {
128                convert_ts_type_scoped(&op.type_annotation, scope, diag)
129            }
130            _ => {
131                diag.warn_with_source(
132                    "Type operator not supported, erasing to JsValue",
133                    format!("{:?}", op.operator),
134                );
135                TypeRef::Unresolved("type operator".to_string())
136            }
137        },
138        TSType::TSTypePredicate(_) => TypeRef::Boolean,
139        TSType::TSTypeQuery(_) => {
140            diag.warn("typeof type queries are not supported, erasing to JsValue");
141            TypeRef::Unresolved("typeof query".to_string())
142        }
143        TSType::TSImportType(_) => {
144            diag.warn("Import types are not supported, erasing to JsValue");
145            TypeRef::Unresolved("import type".to_string())
146        }
147        TSType::TSThisType(_) => TypeRef::Unresolved("this".to_string()),
148
149        // TSNamedTupleMember is also a direct variant of TSType via inherit_variants
150        TSType::TSNamedTupleMember(member) => {
151            // member.element_type is a TSTupleElement, convert it
152            convert_tuple_element_scoped(&member.element_type, scope, diag)
153        }
154
155        TSType::TSParenthesizedType(paren) => {
156            convert_ts_type_scoped(&paren.type_annotation, scope, diag)
157        }
158
159        // JSDoc types
160        TSType::JSDocNullableType(nullable) => {
161            let inner = convert_ts_type_scoped(&nullable.type_annotation, scope, diag);
162            TypeRef::Nullable(Box::new(inner))
163        }
164        TSType::JSDocNonNullableType(non_nullable) => {
165            convert_ts_type_scoped(&non_nullable.type_annotation, scope, diag)
166        }
167        TSType::JSDocUnknownType(_) => TypeRef::Any,
168    }
169}
170
171/// Convert a type reference with type parameter scope.
172fn convert_type_reference_scoped(
173    type_ref: &TSTypeReference<'_>,
174    scope: &TypeParamScope<'_>,
175    diag: &mut DiagnosticCollector,
176) -> TypeRef {
177    let name = type_name_to_string(&type_ref.type_name);
178
179    // If the name is an in-scope type parameter, erase to Any
180    if scope.contains(name.as_str()) {
181        return TypeRef::Any;
182    }
183
184    // Collect generic type arguments if present (field is `type_arguments` in oxc 0.118)
185    let type_args: Vec<TypeRef> = type_ref
186        .type_arguments
187        .as_ref()
188        .map(|args| {
189            args.params
190                .iter()
191                .map(|t| convert_ts_type_scoped(t, scope, diag))
192                .collect()
193        })
194        .unwrap_or_default();
195
196    // Handle well-known generic types
197    match name.as_str() {
198        "Promise" | "PromiseLike" => {
199            let inner = type_args.into_iter().next().unwrap_or(TypeRef::Any);
200            TypeRef::Promise(Box::new(inner))
201        }
202        "Array" | "ReadonlyArray" => {
203            let inner = type_args.into_iter().next().unwrap_or(TypeRef::Any);
204            TypeRef::Array(Box::new(inner))
205        }
206        "Record" => {
207            let mut args = type_args.into_iter();
208            let key = args.next().unwrap_or(TypeRef::String);
209            let value = args.next().unwrap_or(TypeRef::Any);
210            TypeRef::Record(Box::new(key), Box::new(value))
211        }
212        "Map" | "ReadonlyMap" => {
213            let mut args = type_args.into_iter();
214            let key = args.next().unwrap_or(TypeRef::Any);
215            let value = args.next().unwrap_or(TypeRef::Any);
216            TypeRef::Map(Box::new(key), Box::new(value))
217        }
218        "Set" | "ReadonlySet" => {
219            let inner = type_args.into_iter().next().unwrap_or(TypeRef::Any);
220            TypeRef::Set(Box::new(inner))
221        }
222
223        "Int8Array" => TypeRef::Int8Array,
224        "Uint8Array" => TypeRef::Uint8Array,
225        "Uint8ClampedArray" => TypeRef::Uint8ClampedArray,
226        "Int16Array" => TypeRef::Int16Array,
227        "Uint16Array" => TypeRef::Uint16Array,
228        "Int32Array" => TypeRef::Int32Array,
229        "Uint32Array" => TypeRef::Uint32Array,
230        "Float32Array" => TypeRef::Float32Array,
231        "Float64Array" => TypeRef::Float64Array,
232        "BigInt64Array" => TypeRef::BigInt64Array,
233        "BigUint64Array" => TypeRef::BigUint64Array,
234        "ArrayBuffer" | "SharedArrayBuffer" => TypeRef::ArrayBuffer,
235        "ArrayBufferView" => TypeRef::ArrayBufferView,
236        "DataView" => TypeRef::DataView,
237        "Date" => TypeRef::Date,
238        "RegExp" => TypeRef::RegExp,
239        "Error" | "TypeError" | "RangeError" | "SyntaxError" | "DOMException" => TypeRef::Error,
240        "Boolean" => TypeRef::Boolean,
241        "Number" => TypeRef::Number,
242        "String" => TypeRef::String,
243        "Object" => TypeRef::Object,
244        "Symbol" => TypeRef::Symbol,
245        "Function" => TypeRef::Function(crate::ir::FunctionSig {
246            params: vec![],
247            return_type: Box::new(TypeRef::Any),
248        }),
249        "Partial"
250        | "Required"
251        | "Pick"
252        | "Omit"
253        | "Exclude"
254        | "Extract"
255        | "NonNullable"
256        | "ReturnType"
257        | "Parameters"
258        | "ConstructorParameters"
259        | "InstanceType"
260        | "ThisParameterType"
261        | "OmitThisParameter"
262        | "ThisType"
263        | "Awaited" => type_args.into_iter().next().unwrap_or(TypeRef::Object),
264        _ => {
265            if type_args.is_empty() {
266                TypeRef::Named(name)
267            } else {
268                TypeRef::GenericInstantiation(name, type_args)
269            }
270        }
271    }
272}
273
274/// Convert a `TSTypeName` to a string.
275fn type_name_to_string(type_name: &TSTypeName<'_>) -> String {
276    match type_name {
277        TSTypeName::IdentifierReference(ident) => ident.name.to_string(),
278        TSTypeName::QualifiedName(qualified) => {
279            let left = type_name_to_string(&qualified.left);
280            let right = &qualified.right.name;
281            format!("{left}.{right}")
282        }
283        TSTypeName::ThisExpression(_) => "this".to_string(),
284    }
285}
286
287/// Convert a tuple element to a `TypeRef`, with type parameter scope.
288fn convert_tuple_element_scoped(
289    elem: &TSTupleElement<'_>,
290    scope: &TypeParamScope<'_>,
291    diag: &mut DiagnosticCollector,
292) -> TypeRef {
293    match elem {
294        TSTupleElement::TSNamedTupleMember(member) => {
295            convert_tuple_element_scoped(&member.element_type, scope, diag)
296        }
297        TSTupleElement::TSRestType(rest) => {
298            convert_ts_type_scoped(&rest.type_annotation, scope, diag)
299        }
300        TSTupleElement::TSOptionalType(opt) => {
301            let inner = convert_ts_type_scoped(&opt.type_annotation, scope, diag);
302            TypeRef::Nullable(Box::new(inner))
303        }
304        // All remaining variants are TSType variants flattened by inherit_variants!
305        other => {
306            if let Some(ts_type) = tuple_element_as_ts_type(other) {
307                convert_ts_type_scoped(ts_type, scope, diag)
308            } else {
309                diag.warn("Unsupported tuple element type");
310                TypeRef::Any
311            }
312        }
313    }
314}
315
316/// Try to get a reference to the inner TSType from a TSTupleElement.
317/// TSTupleElement inherits all TSType variants via `inherit_variants!`,
318/// and provides `as_ts_type()` to access the underlying TSType.
319fn tuple_element_as_ts_type<'a>(elem: &'a TSTupleElement<'a>) -> Option<&'a TSType<'a>> {
320    elem.as_ts_type()
321}
322
323/// Convert a `TSFunctionType` to our IR `FunctionSig`, with type parameter scope.
324fn convert_function_type_scoped(
325    func: &TSFunctionType<'_>,
326    scope: &TypeParamScope<'_>,
327    diag: &mut DiagnosticCollector,
328) -> crate::ir::FunctionSig {
329    // Extend scope with this function's own type parameters
330    let mut inner_scope = scope.clone();
331    if let Some(tp) = &func.type_parameters {
332        for p in &tp.params {
333            inner_scope.insert(p.name.name.as_str());
334        }
335    }
336
337    let params = convert_formal_params(&func.params, diag);
338    let return_type = convert_ts_type_scoped(&func.return_type.type_annotation, &inner_scope, diag);
339
340    crate::ir::FunctionSig {
341        params,
342        return_type: Box::new(return_type),
343    }
344}
345
346/// Convert oxc `FormalParameters` to our IR `Param` list.
347pub fn convert_formal_params(
348    params: &FormalParameters<'_>,
349    diag: &mut DiagnosticCollector,
350) -> Vec<crate::ir::Param> {
351    let mut result = Vec::new();
352    for (i, param) in params.items.iter().enumerate() {
353        let name = binding_pattern_name(&param.pattern)
354            .map(|n| to_snake_case(&n))
355            .unwrap_or_else(|| format!("arg{i}"));
356
357        // In oxc 0.118, type_annotation and optional are on FormalParameter directly
358        let type_ref = param
359            .type_annotation
360            .as_ref()
361            .map(|ann| convert_ts_type(&ann.type_annotation, diag))
362            .unwrap_or(TypeRef::Any);
363
364        let optional = param.optional;
365
366        result.push(crate::ir::Param {
367            name,
368            type_ref,
369            optional,
370            variadic: false,
371        });
372    }
373
374    // Handle rest parameter
375    if let Some(rest) = &params.rest {
376        let name = binding_pattern_name(&rest.rest.argument).unwrap_or_else(|| "rest".to_string());
377
378        let type_ref = rest
379            .type_annotation
380            .as_ref()
381            .map(|ann| convert_ts_type(&ann.type_annotation, diag))
382            .unwrap_or(TypeRef::Array(Box::new(TypeRef::Any)));
383
384        result.push(crate::ir::Param {
385            name,
386            type_ref,
387            optional: false,
388            variadic: true,
389        });
390    }
391
392    result
393}
394
395/// Extract a name from a binding pattern (only handles simple identifier patterns).
396/// In oxc 0.118, BindingPattern is an enum directly.
397fn binding_pattern_name(pattern: &BindingPattern<'_>) -> Option<String> {
398    match pattern {
399        BindingPattern::BindingIdentifier(ident) => Some(ident.name.to_string()),
400        _ => None,
401    }
402}
403
404/// Convert a `TSLiteralType` to our IR `TypeRef`.
405fn convert_literal_type(lit: &TSLiteralType<'_>, _diag: &mut DiagnosticCollector) -> TypeRef {
406    match &lit.literal {
407        TSLiteral::BooleanLiteral(b) => TypeRef::BooleanLiteral(b.value),
408        TSLiteral::NumericLiteral(n) => TypeRef::NumberLiteral(n.value),
409        TSLiteral::StringLiteral(s) => TypeRef::StringLiteral(s.value.to_string()),
410        TSLiteral::BigIntLiteral(_) => TypeRef::BigInt,
411        TSLiteral::TemplateLiteral(_) => TypeRef::String,
412        TSLiteral::UnaryExpression(_) => TypeRef::Number,
413    }
414}
415
416/// Simplify a union type.
417fn simplify_union(types: Vec<TypeRef>) -> TypeRef {
418    let mut non_null_types = Vec::new();
419    let mut has_null = false;
420    let mut has_undefined = false;
421
422    for ty in types {
423        match ty {
424            TypeRef::Null => has_null = true,
425            TypeRef::Undefined => has_undefined = true,
426            TypeRef::Void => has_undefined = true,
427            other => non_null_types.push(other),
428        }
429    }
430
431    let core_type = if non_null_types.len() == 1 {
432        non_null_types.pop().unwrap()
433    } else if non_null_types.is_empty() {
434        return TypeRef::Null;
435    } else {
436        TypeRef::Union(non_null_types)
437    };
438
439    if has_null || has_undefined {
440        TypeRef::Nullable(Box::new(core_type))
441    } else {
442        core_type
443    }
444}
445
446/// Convert `TSTypeParameterDeclaration` to our IR `TypeParam` list.
447pub fn convert_type_params(
448    type_params: Option<&oxc_allocator::Box<'_, TSTypeParameterDeclaration<'_>>>,
449    diag: &mut DiagnosticCollector,
450) -> Vec<crate::ir::TypeParam> {
451    type_params
452        .map(|tp| {
453            tp.params
454                .iter()
455                .map(|p| crate::ir::TypeParam {
456                    name: p.name.to_string(),
457                    constraint: p.constraint.as_ref().map(|c| convert_ts_type(c, diag)),
458                    default: p.default.as_ref().map(|d| convert_ts_type(d, diag)),
459                })
460                .collect()
461        })
462        .unwrap_or_default()
463}