Skip to main content

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