Skip to main content

mago_codex/ttype/
builder.rs

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