mago_codex/ttype/
builder.rs

1use std::borrow::Cow;
2use std::collections::BTreeMap;
3
4use mago_atom::Atom;
5use mago_atom::atom;
6use mago_atom::i64_atom;
7use mago_names::kind::NameKind;
8use mago_names::scope::NamespaceScope;
9use mago_span::HasSpan;
10use mago_span::Span;
11use mago_type_syntax;
12use mago_type_syntax::ast::object::ObjectType;
13use mago_type_syntax::ast::*;
14
15use crate::misc::GenericParent;
16use crate::ttype::TType;
17use crate::ttype::atomic::TAtomic;
18use crate::ttype::atomic::array::TArray;
19use crate::ttype::atomic::array::keyed::TKeyedArray;
20use crate::ttype::atomic::callable::TCallable;
21use crate::ttype::atomic::callable::TCallableSignature;
22use crate::ttype::atomic::callable::parameter::TCallableParameter;
23use crate::ttype::atomic::derived::TDerived;
24use crate::ttype::atomic::derived::key_of::TKeyOf;
25use crate::ttype::atomic::derived::properties_of::TPropertiesOf;
26use crate::ttype::atomic::derived::value_of::TValueOf;
27use crate::ttype::atomic::object::TObject;
28use crate::ttype::atomic::object::named::TNamedObject;
29use crate::ttype::atomic::reference::TReferenceMemberSelector;
30use crate::ttype::atomic::scalar::TScalar;
31use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
32use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
33use crate::ttype::error::TypeError;
34use crate::ttype::get_arraykey;
35use crate::ttype::get_bool;
36use crate::ttype::get_closed_resource;
37use crate::ttype::get_false;
38use crate::ttype::get_float;
39use crate::ttype::get_int;
40use crate::ttype::get_literal_float;
41use crate::ttype::get_literal_int;
42use crate::ttype::get_literal_string;
43use crate::ttype::get_lowercase_string;
44use crate::ttype::get_mixed;
45use crate::ttype::get_negative_int;
46use crate::ttype::get_never;
47use crate::ttype::get_non_empty_lowercase_string;
48use crate::ttype::get_non_empty_string;
49use crate::ttype::get_non_empty_unspecified_literal_string;
50use crate::ttype::get_non_negative_int;
51use crate::ttype::get_null;
52use crate::ttype::get_nullable_float;
53use crate::ttype::get_nullable_int;
54use crate::ttype::get_nullable_object;
55use crate::ttype::get_nullable_scalar;
56use crate::ttype::get_nullable_string;
57use crate::ttype::get_numeric;
58use crate::ttype::get_numeric_string;
59use crate::ttype::get_open_resource;
60use crate::ttype::get_positive_int;
61use crate::ttype::get_resource;
62use crate::ttype::get_scalar;
63use crate::ttype::get_string;
64use crate::ttype::get_true;
65use crate::ttype::get_truthy_string;
66use crate::ttype::get_unspecified_literal_string;
67use crate::ttype::get_void;
68use crate::ttype::resolution::TypeResolutionContext;
69use crate::ttype::union::TUnion;
70use crate::ttype::wrap_atomic;
71
72use super::atomic::array::key::ArrayKey;
73use super::atomic::array::list::TList;
74use super::atomic::conditional::TConditional;
75use super::atomic::generic::TGenericParameter;
76use super::atomic::iterable::TIterable;
77use super::atomic::reference::TReference;
78use super::atomic::scalar::int::TInteger;
79
80/// Parses a type string (typically from a PHPDoc comment) and resolves it
81/// into a semantic `TUnion` type representation.
82///
83/// This function orchestrates the two main phases:
84///
85/// 1. Parsing the raw string into an Abstract Syntax Tree (AST) using the `mago_type_syntax` crate.
86/// 2. Converting the AST into a `TUnion`, resolving names, templates, and
87///    keywords into their semantic counterparts.
88///
89/// # Arguments
90///
91/// * `type_string` - The raw string slice containing the type to parse (e.g., `"int|string"`).
92/// * `span` - The original `Span` of the `type_string` within its source file.
93///   This is crucial for accurate error reporting and position tracking.
94/// * `scope` - The `NamespaceScope` active at the location of the type string.
95///   Used during conversion to resolve unqualified names, aliases (`use` statements),
96///   and namespace-relative names.
97/// * `type_context` - The context providing information about currently defined
98///   template parameters (e.g., from `@template` tags). Needed
99///   during conversion to resolve template parameter references.
100/// * `classname` - An optional `Atom` representing the fully qualified name
101///   of the current class context. Used during conversion to resolve
102///   `self` type references. Should be `None` if not in a class context.
103///
104/// # Returns
105///
106/// * `Ok(TUnion)`: The resolved semantic type representation on success.
107/// * `Err(TypeError)`: If any parsing or (future) conversion error occurs.
108///   The `TypeError` can encapsulate errors originating from the
109///   syntax parsing phase.
110pub fn get_type_from_string(
111    type_string: &str,
112    span: Span,
113    scope: &NamespaceScope,
114    type_context: &TypeResolutionContext,
115    classname: Option<Atom>,
116) -> Result<TUnion, TypeError> {
117    let ast = mago_type_syntax::parse_str(span, type_string)?;
118
119    get_union_from_type_ast(&ast, scope, type_context, classname)
120}
121
122#[inline]
123pub fn get_union_from_type_ast<'i>(
124    ttype: &Type<'i>,
125    scope: &NamespaceScope,
126    type_context: &TypeResolutionContext,
127    classname: Option<Atom>,
128) -> Result<TUnion, TypeError> {
129    Ok(match ttype {
130        Type::Parenthesized(parenthesized_type) => {
131            get_union_from_type_ast(&parenthesized_type.inner, scope, type_context, classname)?
132        }
133        Type::Nullable(nullable_type) => match nullable_type.inner.as_ref() {
134            Type::Null(_) => get_null(),
135            Type::String(_) => get_nullable_string(),
136            Type::Int(_) => get_nullable_int(),
137            Type::Float(_) => get_nullable_float(),
138            Type::Object(_) => get_nullable_object(),
139            Type::Scalar(_) => get_nullable_scalar(),
140            _ => get_union_from_type_ast(&nullable_type.inner, scope, type_context, classname)?.as_nullable(),
141        },
142        Type::Union(UnionType { left, right, .. }) if matches!(left.as_ref(), Type::Null(_)) => match right.as_ref() {
143            Type::Null(_) => get_null(),
144            Type::String(_) => get_nullable_string(),
145            Type::Int(_) => get_nullable_int(),
146            Type::Float(_) => get_nullable_float(),
147            Type::Object(_) => get_nullable_object(),
148            Type::Scalar(_) => get_nullable_scalar(),
149            _ => get_union_from_type_ast(right, scope, type_context, classname)?.as_nullable(),
150        },
151        Type::Union(UnionType { left, right, .. }) if matches!(right.as_ref(), Type::Null(_)) => match left.as_ref() {
152            Type::Null(_) => get_null(),
153            Type::String(_) => get_nullable_string(),
154            Type::Int(_) => get_nullable_int(),
155            Type::Float(_) => get_nullable_float(),
156            Type::Object(_) => get_nullable_object(),
157            Type::Scalar(_) => get_nullable_scalar(),
158            _ => get_union_from_type_ast(left, scope, type_context, classname)?.as_nullable(),
159        },
160        Type::Union(union_type) => {
161            let left = get_union_from_type_ast(&union_type.left, scope, type_context, classname)?;
162            let right = get_union_from_type_ast(&union_type.right, scope, type_context, classname)?;
163
164            let combined_types: Vec<TAtomic> = left.types.iter().chain(right.types.iter()).cloned().collect();
165
166            TUnion::from_vec(combined_types)
167        }
168        Type::Intersection(intersection) => {
169            let left = get_union_from_type_ast(&intersection.left, scope, type_context, classname)?;
170            let right = get_union_from_type_ast(&intersection.right, scope, type_context, classname)?;
171
172            let left_str = left.get_id();
173            let right_str = right.get_id();
174
175            let left_types = left.types.into_owned();
176            let right_types = right.types.into_owned();
177            let mut intersection_types = vec![];
178            for left_type in left_types {
179                if !left_type.can_be_intersected() {
180                    return Err(TypeError::InvalidType(
181                        ttype.to_string(),
182                        format!(
183                            "Type `{}` used in intersection cannot be intersected with another type ( `{}` )",
184                            left_type.get_id(),
185                            right_str,
186                        ),
187                        ttype.span(),
188                    ));
189                }
190
191                for right_type in &right_types {
192                    let mut intersection = left_type.clone();
193
194                    if !intersection.add_intersection_type(right_type.clone()) {
195                        return Err(TypeError::InvalidType(
196                            ttype.to_string(),
197                            format!(
198                                "Type `{}` used in intersection cannot be intersected with another type ( `{}` )",
199                                right_type.get_id(),
200                                left_str,
201                            ),
202                            ttype.span(),
203                        ));
204                    }
205
206                    intersection_types.push(intersection);
207                }
208            }
209
210            TUnion::from_vec(intersection_types)
211        }
212        Type::Slice(slice) => wrap_atomic(get_array_type_from_ast(
213            None,
214            Some(slice.inner.as_ref()),
215            false,
216            scope,
217            type_context,
218            classname,
219        )?),
220        Type::Array(ArrayType { parameters, .. }) | Type::AssociativeArray(AssociativeArrayType { parameters, .. }) => {
221            let (key, value) = match parameters {
222                Some(parameters) => {
223                    let key = parameters.entries.first().map(|g| &g.inner);
224                    let value = parameters.entries.get(1).map(|g| &g.inner);
225
226                    (key, value)
227                }
228                None => (None, None),
229            };
230
231            wrap_atomic(get_array_type_from_ast(key, value, false, scope, type_context, classname)?)
232        }
233        Type::NonEmptyArray(non_empty_array) => {
234            let (key, value) = match &non_empty_array.parameters {
235                Some(parameters) => {
236                    let key = parameters.entries.first().map(|g| &g.inner);
237                    let value = parameters.entries.get(1).map(|g| &g.inner);
238
239                    (key, value)
240                }
241                None => (None, None),
242            };
243
244            wrap_atomic(get_array_type_from_ast(key, value, true, scope, type_context, classname)?)
245        }
246        Type::List(list_type) => {
247            let value = list_type.parameters.as_ref().and_then(|p| p.entries.first().map(|g| &g.inner));
248
249            wrap_atomic(get_list_type_from_ast(value, false, scope, type_context, classname)?)
250        }
251        Type::NonEmptyList(non_empty_list_type) => {
252            let value = non_empty_list_type.parameters.as_ref().and_then(|p| p.entries.first().map(|g| &g.inner));
253
254            wrap_atomic(get_list_type_from_ast(value, true, scope, type_context, classname)?)
255        }
256        Type::ClassString(class_string_type) => get_class_string_type_from_ast(
257            class_string_type.span(),
258            TClassLikeStringKind::Class,
259            &class_string_type.parameter,
260            scope,
261            type_context,
262            classname,
263        )?,
264        Type::InterfaceString(interface_string_type) => get_class_string_type_from_ast(
265            interface_string_type.span(),
266            TClassLikeStringKind::Interface,
267            &interface_string_type.parameter,
268            scope,
269            type_context,
270            classname,
271        )?,
272        Type::EnumString(enum_string_type) => get_class_string_type_from_ast(
273            enum_string_type.span(),
274            TClassLikeStringKind::Enum,
275            &enum_string_type.parameter,
276            scope,
277            type_context,
278            classname,
279        )?,
280        Type::TraitString(trait_string_type) => get_class_string_type_from_ast(
281            trait_string_type.span(),
282            TClassLikeStringKind::Trait,
283            &trait_string_type.parameter,
284            scope,
285            type_context,
286            classname,
287        )?,
288        Type::MemberReference(member_reference) => {
289            let class_like_name = if member_reference.class.value.eq_ignore_ascii_case("self")
290                || member_reference.class.value.eq_ignore_ascii_case("static")
291                || member_reference.class.value.eq("this")
292                || member_reference.class.value.eq("$this")
293            {
294                let Some(classname) = classname else {
295                    return Err(TypeError::InvalidType(
296                        ttype.to_string(),
297                        "Cannot resolve `self` type reference outside of a class context".to_string(),
298                        member_reference.span(),
299                    ));
300                };
301
302                classname
303            } else {
304                let (class_like_name, _) = scope.resolve(NameKind::Default, member_reference.class.value);
305
306                atom(&class_like_name)
307            };
308
309            let member_selector = match member_reference.member {
310                MemberReferenceSelector::Wildcard(_) => TReferenceMemberSelector::Wildcard,
311                MemberReferenceSelector::Identifier(identifier) => {
312                    TReferenceMemberSelector::Identifier(atom(identifier.value))
313                }
314                MemberReferenceSelector::StartsWith(identifier, _) => {
315                    TReferenceMemberSelector::StartsWith(atom(identifier.value))
316                }
317                MemberReferenceSelector::EndsWith(_, identifier) => {
318                    TReferenceMemberSelector::EndsWith(atom(identifier.value))
319                }
320            };
321
322            wrap_atomic(TAtomic::Reference(TReference::Member { class_like_name, member_selector }))
323        }
324        Type::Object(object_type) => wrap_atomic(get_object_from_ast(object_type, scope, type_context, classname)?),
325        Type::Shape(shape_type) => wrap_atomic(get_shape_from_ast(shape_type, scope, type_context, classname)?),
326        Type::Callable(callable_type) => {
327            wrap_atomic(get_callable_from_ast(callable_type, scope, type_context, classname)?)
328        }
329        Type::Reference(reference_type) => wrap_atomic(get_reference_from_ast(
330            &reference_type.identifier,
331            reference_type.parameters.as_ref(),
332            scope,
333            type_context,
334            classname,
335        )?),
336        Type::Mixed(_) => get_mixed(),
337        Type::Null(_) => get_null(),
338        Type::Void(_) => get_void(),
339        Type::Never(_) => get_never(),
340        Type::Resource(_) => get_resource(),
341        Type::ClosedResource(_) => get_closed_resource(),
342        Type::OpenResource(_) => get_open_resource(),
343        Type::True(_) => get_true(),
344        Type::False(_) => get_false(),
345        Type::Bool(_) => get_bool(),
346        Type::Float(_) => get_float(),
347        Type::Int(_) => get_int(),
348        Type::String(_) => get_string(),
349        Type::ArrayKey(_) => get_arraykey(),
350        Type::Numeric(_) => get_numeric(),
351        Type::Scalar(_) => get_scalar(),
352        Type::NumericString(_) => get_numeric_string(),
353        Type::NonEmptyString(_) => get_non_empty_string(),
354        Type::TruthyString(_) | Type::NonFalsyString(_) => get_truthy_string(),
355        Type::UnspecifiedLiteralString(_) => get_unspecified_literal_string(),
356        Type::NonEmptyUnspecifiedLiteralString(_) => get_non_empty_unspecified_literal_string(),
357        Type::NonEmptyLowercaseString(_) => get_non_empty_lowercase_string(),
358        Type::LowercaseString(_) => get_lowercase_string(),
359        Type::LiteralFloat(lit) => get_literal_float(*lit.value),
360        Type::LiteralInt(lit) => get_literal_int(lit.value as i64),
361        Type::LiteralString(lit) => get_literal_string(atom(lit.value)),
362        Type::Negated(negated) => match negated.number {
363            LiteralIntOrFloatType::Int(lit) => get_literal_int(-(lit.value as i64)),
364            LiteralIntOrFloatType::Float(lit) => get_literal_float(-(*lit.value)),
365        },
366        Type::Posited(posited) => match posited.number {
367            LiteralIntOrFloatType::Int(lit) => get_literal_int(lit.value as i64),
368            LiteralIntOrFloatType::Float(lit) => get_literal_float(*lit.value),
369        },
370        Type::Iterable(iterable) => match iterable.parameters.as_ref() {
371            Some(parameters) => match parameters.entries.len() {
372                0 => wrap_atomic(TAtomic::Iterable(TIterable::mixed())),
373                1 => {
374                    let value_type =
375                        get_union_from_type_ast(&parameters.entries[0].inner, scope, type_context, classname)?;
376
377                    wrap_atomic(TAtomic::Iterable(TIterable::of_value(Box::new(value_type))))
378                }
379                _ => {
380                    let key_type =
381                        get_union_from_type_ast(&parameters.entries[0].inner, scope, type_context, classname)?;
382
383                    let value_type =
384                        get_union_from_type_ast(&parameters.entries[1].inner, scope, type_context, classname)?;
385
386                    wrap_atomic(TAtomic::Iterable(TIterable::new(Box::new(key_type), Box::new(value_type))))
387                }
388            },
389            None => wrap_atomic(TAtomic::Iterable(TIterable::mixed())),
390        },
391        Type::PositiveInt(_) => get_positive_int(),
392        Type::NegativeInt(_) => get_negative_int(),
393        Type::NonPositiveInt(_) => get_positive_int(),
394        Type::NonNegativeInt(_) => get_non_negative_int(),
395        Type::IntRange(range) => {
396            let min = match range.min {
397                IntOrKeyword::NegativeInt { int, .. } => Some(-(int.value as i64)),
398                IntOrKeyword::Int(literal_int_type) => Some(literal_int_type.value as i64),
399                IntOrKeyword::Keyword(_) => None,
400            };
401
402            let max = match range.max {
403                IntOrKeyword::NegativeInt { int, .. } => Some(-(int.value as i64)),
404                IntOrKeyword::Int(literal_int_type) => Some(literal_int_type.value as i64),
405                IntOrKeyword::Keyword(_) => None,
406            };
407
408            if let (Some(min_value), Some(max_value)) = (min, max)
409                && min_value > max_value
410            {
411                return Err(TypeError::InvalidType(
412                    ttype.to_string(),
413                    "Minimum value of an int range cannot be greater than maximum value".to_string(),
414                    ttype.span(),
415                ));
416            }
417
418            TUnion::from_single(Cow::Owned(TAtomic::Scalar(TScalar::Integer(TInteger::from_bounds(min, max)))))
419        }
420        Type::Conditional(conditional) => TUnion::from_single(Cow::Owned(TAtomic::Conditional(TConditional::new(
421            Box::new(get_union_from_type_ast(&conditional.subject, scope, type_context, classname)?),
422            Box::new(get_union_from_type_ast(&conditional.target, scope, type_context, classname)?),
423            Box::new(get_union_from_type_ast(&conditional.then, scope, type_context, classname)?),
424            Box::new(get_union_from_type_ast(&conditional.otherwise, scope, type_context, classname)?),
425            conditional.is_negated(),
426        )))),
427        Type::Variable(variable_type) => TUnion::from_single(Cow::Owned(TAtomic::Variable(atom(variable_type.value)))),
428        Type::KeyOf(key_of_type) => {
429            let target = get_union_from_type_ast(&key_of_type.parameter.entry.inner, scope, type_context, classname)?;
430
431            let mut atomics = vec![];
432            for target_type in target.types.into_owned() {
433                atomics.push(TAtomic::Derived(TDerived::KeyOf(TKeyOf::new(Box::new(target_type)))));
434            }
435
436            TUnion::from_vec(atomics)
437        }
438        Type::ValueOf(value_of_type) => {
439            let target = get_union_from_type_ast(&value_of_type.parameter.entry.inner, scope, type_context, classname)?;
440
441            let mut atomics = vec![];
442            for target_type in target.types.into_owned() {
443                atomics.push(TAtomic::Derived(TDerived::ValueOf(TValueOf::new(Box::new(target_type)))));
444            }
445
446            TUnion::from_vec(atomics)
447        }
448        Type::PropertiesOf(properties_of_type) => {
449            let target =
450                get_union_from_type_ast(&properties_of_type.parameter.entry.inner, scope, type_context, classname)?;
451
452            let mut atomics = vec![];
453            for target_type in target.types.into_owned() {
454                atomics.push(TAtomic::Derived(TDerived::PropertiesOf(match properties_of_type.filter {
455                    PropertiesOfFilter::All => TPropertiesOf::new(Box::new(target_type)),
456                    PropertiesOfFilter::Public => TPropertiesOf::public(Box::new(target_type)),
457                    PropertiesOfFilter::Protected => TPropertiesOf::protected(Box::new(target_type)),
458                    PropertiesOfFilter::Private => TPropertiesOf::private(Box::new(target_type)),
459                })));
460            }
461
462            TUnion::from_vec(atomics)
463        }
464        _ => {
465            return Err(TypeError::UnsupportedType(ttype.to_string(), ttype.span()));
466        }
467    })
468}
469
470#[inline]
471fn get_object_from_ast(
472    object: &ObjectType<'_>,
473    scope: &NamespaceScope,
474    type_context: &TypeResolutionContext,
475    classname: Option<Atom>,
476) -> Result<TAtomic, TypeError> {
477    let Some(properties) = object.properties.as_ref() else {
478        return Ok(TAtomic::Object(TObject::Any));
479    };
480
481    let mut known_properties = BTreeMap::new();
482    for property in &properties.fields {
483        let property_is_optional = property.is_optional();
484
485        let Some(field_key) = property.key.as_ref() else {
486            continue;
487        };
488
489        let key = match field_key.key {
490            ShapeKey::String { value, .. } => atom(value),
491            ShapeKey::Integer { value, .. } => i64_atom(value),
492        };
493
494        let property_type = get_union_from_type_ast(&property.value, scope, type_context, classname)?;
495
496        known_properties.insert(key, (property_is_optional, property_type));
497    }
498
499    Ok(TAtomic::Object(TObject::new_with_properties(properties.ellipsis.is_none(), known_properties)))
500}
501
502#[inline]
503fn get_shape_from_ast(
504    shape: &ShapeType<'_>,
505    scope: &NamespaceScope,
506    type_context: &TypeResolutionContext,
507    classname: Option<Atom>,
508) -> Result<TAtomic, TypeError> {
509    if shape.kind.is_list() {
510        let mut list = TList::new(match &shape.additional_fields {
511            Some(additional_fields) => match &additional_fields.parameters {
512                Some(parameters) => Box::new(if let Some(k) = parameters.entries.first().map(|g| &g.inner) {
513                    get_union_from_type_ast(k, scope, type_context, classname)?
514                } else {
515                    get_mixed()
516                }),
517                None => Box::new(get_mixed()),
518            },
519            None => Box::new(get_never()),
520        });
521
522        list.known_elements = Some({
523            let mut tree = BTreeMap::new();
524            let mut next_offset: usize = 0;
525
526            for field in &shape.fields {
527                let field_is_optional = field.is_optional();
528
529                let offset = match field.key.as_ref() {
530                    Some(field_key) => {
531                        let array_key = match field_key.key {
532                            ShapeKey::String { value, .. } => ArrayKey::String(atom(value)),
533                            ShapeKey::Integer { value, .. } => ArrayKey::Integer(value),
534                        };
535
536                        if let ArrayKey::Integer(offset) = array_key {
537                            if offset > 0 && (offset as usize) == next_offset {
538                                next_offset += 1;
539
540                                offset as usize
541                            } else {
542                                return Err(TypeError::InvalidType(
543                                    shape.to_string(),
544                                    "List shape keys must be sequential".to_string(),
545                                    field_key.span(),
546                                ));
547                            }
548                        } else {
549                            return Err(TypeError::InvalidType(
550                                shape.to_string(),
551                                "List shape keys are expected to be integers".to_string(),
552                                field_key.span(),
553                            ));
554                        }
555                    }
556                    None => {
557                        let offset = next_offset;
558
559                        next_offset += 1;
560
561                        offset
562                    }
563                };
564
565                let field_value_type = get_union_from_type_ast(&field.value, scope, type_context, classname)?;
566
567                tree.insert(offset, (field_is_optional, field_value_type));
568            }
569
570            tree
571        });
572
573        list.non_empty = shape.has_non_optional_fields() || shape.kind.is_non_empty();
574
575        Ok(TAtomic::Array(TArray::List(list)))
576    } else {
577        let mut keyed_array = TKeyedArray::new();
578
579        keyed_array.parameters = match &shape.additional_fields {
580            Some(additional_fields) => Some(match &additional_fields.parameters {
581                Some(parameters) => (
582                    Box::new(if let Some(k) = parameters.entries.first().map(|g| &g.inner) {
583                        get_union_from_type_ast(k, scope, type_context, classname)?
584                    } else {
585                        get_mixed()
586                    }),
587                    Box::new(if let Some(v) = parameters.entries.get(1).map(|g| &g.inner) {
588                        get_union_from_type_ast(v, scope, type_context, classname)?
589                    } else {
590                        get_mixed()
591                    }),
592                ),
593                None => (Box::new(get_arraykey()), Box::new(get_mixed())),
594            }),
595            None => None,
596        };
597
598        keyed_array.known_items = Some({
599            let mut tree = BTreeMap::new();
600            let mut next_offset = 0;
601
602            for field in &shape.fields {
603                let field_is_optional = field.is_optional();
604
605                let array_key = match field.key.as_ref() {
606                    Some(field_key) => {
607                        let array_key = match field_key.key {
608                            ShapeKey::String { value, .. } => ArrayKey::String(atom(value)),
609                            ShapeKey::Integer { value, .. } => ArrayKey::Integer(value),
610                        };
611
612                        if let ArrayKey::Integer(offset) = array_key
613                            && offset >= next_offset
614                        {
615                            next_offset = offset + 1;
616                        }
617
618                        array_key
619                    }
620                    None => {
621                        let array_key = ArrayKey::Integer(next_offset);
622
623                        next_offset += 1;
624
625                        array_key
626                    }
627                };
628
629                let field_value_type = get_union_from_type_ast(&field.value, scope, type_context, classname)?;
630
631                tree.insert(array_key, (field_is_optional, field_value_type));
632            }
633
634            tree
635        });
636
637        keyed_array.non_empty = shape.has_non_optional_fields() || shape.kind.is_non_empty();
638
639        Ok(TAtomic::Array(TArray::Keyed(keyed_array)))
640    }
641}
642
643#[inline]
644fn get_callable_from_ast(
645    callable: &CallableType<'_>,
646    scope: &NamespaceScope,
647    type_context: &TypeResolutionContext,
648    classname: Option<Atom>,
649) -> Result<TAtomic, TypeError> {
650    let mut parameters = vec![];
651    let mut return_type = None;
652
653    if let Some(specification) = &callable.specification {
654        for parameter_ast in specification.parameters.entries.iter() {
655            let parameter_type = if let Some(parameter_type) = &parameter_ast.parameter_type {
656                get_union_from_type_ast(parameter_type, scope, type_context, classname)?
657            } else {
658                get_mixed()
659            };
660
661            parameters.push(TCallableParameter::new(
662                Some(Box::new(parameter_type)),
663                false,
664                parameter_ast.is_variadic(),
665                parameter_ast.is_optional(),
666            ));
667        }
668
669        if let Some(ret) = specification.return_type.as_ref() {
670            return_type = Some(get_union_from_type_ast(&ret.return_type, scope, type_context, classname)?);
671        }
672    } else {
673        // `callable` without a specification should be treated the same as
674        // `callable(mixed...): mixed`
675        parameters.push(TCallableParameter::new(Some(Box::new(get_mixed())), false, true, false));
676        return_type = Some(get_mixed());
677    }
678
679    Ok(TAtomic::Callable(TCallable::Signature(
680        TCallableSignature::new(callable.kind.is_pure(), callable.kind.is_closure())
681            .with_parameters(parameters)
682            .with_return_type(return_type.map(Box::new)),
683    )))
684}
685
686#[inline]
687fn get_reference_from_ast<'i>(
688    reference_identifier: &Identifier<'i>,
689    generics: Option<&GenericParameters<'i>>,
690    scope: &NamespaceScope,
691    type_context: &TypeResolutionContext,
692    classname: Option<Atom>,
693) -> Result<TAtomic, TypeError> {
694    let reference_name = reference_identifier.value;
695
696    let mut is_this = false;
697    let mut is_named_object = false;
698    let fq_reference_name_id = if reference_name == "this" || reference_name == "static" || reference_name == "self" {
699        is_named_object = true;
700        is_this = reference_name != "self";
701
702        classname.unwrap_or_else(|| atom("static"))
703    } else {
704        if let Some(defining_entities) = type_context.get_template_definition(reference_name)
705            && generics.is_none()
706        {
707            return Ok(get_template_atomic(defining_entities, atom(reference_name)));
708        }
709
710        let (fq_reference_name, _) = scope.resolve(NameKind::Default, reference_name);
711
712        // `Closure` -> `Closure(mixed...): mixed`
713        if fq_reference_name.eq_ignore_ascii_case("Closure") && generics.is_none() {
714            return Ok(TAtomic::Callable(TCallable::Signature(
715                TCallableSignature::new(false, true)
716                    .with_parameters(vec![TCallableParameter::new(Some(Box::new(get_mixed())), false, true, false)])
717                    .with_return_type(Some(Box::new(get_mixed()))),
718            )));
719        }
720
721        atom(&fq_reference_name)
722    };
723
724    let mut type_parameters = None;
725    if let Some(generics) = generics {
726        let mut parameters = vec![];
727        for generic in &generics.entries {
728            let generic_type = get_union_from_type_ast(&generic.inner, scope, type_context, classname)?;
729
730            parameters.push(generic_type);
731        }
732
733        type_parameters = Some(parameters);
734    }
735
736    let is_generator = fq_reference_name_id.eq_ignore_ascii_case("Generator");
737
738    let is_iterator = is_generator
739        || fq_reference_name_id.eq_ignore_ascii_case("Iterator")
740        || fq_reference_name_id.eq_ignore_ascii_case("IteratorAggregate")
741        || fq_reference_name_id.eq_ignore_ascii_case("Traversable");
742
743    'iterator: {
744        if !is_iterator {
745            break 'iterator;
746        }
747
748        let Some(type_parameters) = &mut type_parameters else {
749            type_parameters = Some(vec![get_mixed(), get_mixed()]);
750
751            break 'iterator;
752        };
753
754        if type_parameters.len() == 1 {
755            type_parameters.insert(0, get_mixed());
756        } else if type_parameters.is_empty() {
757            type_parameters.push(get_mixed());
758            type_parameters.push(get_mixed());
759        }
760
761        if !is_generator {
762            break 'iterator;
763        }
764
765        while type_parameters.len() < 4 {
766            type_parameters.push(get_mixed());
767        }
768    }
769
770    if is_named_object {
771        Ok(TAtomic::Object(TObject::Named(TNamedObject {
772            name: fq_reference_name_id,
773            type_parameters,
774            intersection_types: None,
775            is_this,
776            remapped_parameters: false,
777        })))
778    } else {
779        Ok(TAtomic::Reference(TReference::Symbol {
780            name: fq_reference_name_id,
781            parameters: type_parameters,
782            intersection_types: None,
783        }))
784    }
785}
786
787#[inline]
788fn get_array_type_from_ast<'i, 'p>(
789    mut key: Option<&'p Type<'i>>,
790    mut value: Option<&'p Type<'i>>,
791    non_empty: bool,
792    scope: &NamespaceScope,
793    type_context: &TypeResolutionContext,
794    classname: Option<Atom>,
795) -> Result<TAtomic, TypeError> {
796    if key.is_some() && value.is_none() {
797        std::mem::swap(&mut key, &mut value);
798    }
799
800    let mut array = TKeyedArray::new_with_parameters(
801        Box::new(if let Some(k) = key {
802            get_union_from_type_ast(k, scope, type_context, classname)?
803        } else {
804            get_arraykey()
805        }),
806        Box::new(if let Some(v) = value {
807            get_union_from_type_ast(v, scope, type_context, classname)?
808        } else {
809            get_mixed()
810        }),
811    );
812
813    array.non_empty = non_empty;
814
815    Ok(TAtomic::Array(TArray::Keyed(array)))
816}
817
818#[inline]
819fn get_list_type_from_ast(
820    value: Option<&Type<'_>>,
821    non_empty: bool,
822    scope: &NamespaceScope,
823    type_context: &TypeResolutionContext,
824    classname: Option<Atom>,
825) -> Result<TAtomic, TypeError> {
826    Ok(TAtomic::Array(TArray::List(TList {
827        element_type: Box::new(if let Some(v) = value {
828            get_union_from_type_ast(v, scope, type_context, classname)?
829        } else {
830            get_mixed()
831        }),
832        known_count: None,
833        known_elements: None,
834        non_empty,
835    })))
836}
837
838#[inline]
839fn get_class_string_type_from_ast(
840    span: Span,
841    kind: TClassLikeStringKind,
842    parameter: &Option<SingleGenericParameter<'_>>,
843    scope: &NamespaceScope,
844    type_context: &TypeResolutionContext,
845    classname: Option<Atom>,
846) -> Result<TUnion, TypeError> {
847    Ok(match parameter {
848        Some(parameter) => {
849            let constraint_union = get_union_from_type_ast(&parameter.entry.inner, scope, type_context, classname)?;
850
851            let mut class_strings = vec![];
852            for constraint in constraint_union.types.into_owned() {
853                match constraint {
854                    TAtomic::Object(TObject::Named(_))
855                    | TAtomic::Object(TObject::Enum(_))
856                    | TAtomic::Reference(TReference::Symbol { .. }) => class_strings
857                        .push(TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::of_type(kind, constraint)))),
858                    TAtomic::GenericParameter(TGenericParameter {
859                        parameter_name,
860                        defining_entity,
861                        constraint,
862                        ..
863                    }) => {
864                        for constraint_atomic in constraint.types.into_owned() {
865                            class_strings.push(TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::generic(
866                                kind,
867                                parameter_name,
868                                defining_entity,
869                                constraint_atomic,
870                            ))));
871                        }
872                    }
873                    _ => {
874                        return Err(TypeError::InvalidType(
875                            kind.to_string(),
876                            format!(
877                                "class string parameter must target an object type, found `{}`.",
878                                constraint.get_id()
879                            ),
880                            span,
881                        ));
882                    }
883                }
884            }
885
886            TUnion::from_vec(class_strings)
887        }
888        None => wrap_atomic(TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::any(kind)))),
889    })
890}
891
892#[inline]
893fn get_template_atomic(defining_entities: &[(GenericParent, TUnion)], parameter_name: Atom) -> TAtomic {
894    let (defining_entity, constraint) = &defining_entities[0];
895
896    TAtomic::GenericParameter(TGenericParameter {
897        parameter_name,
898        constraint: Box::new(constraint.clone()),
899        defining_entity: *defining_entity,
900        intersection_types: None,
901    })
902}