mago_codex/ttype/
builder.rs

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