Skip to main content

mago_codex/ttype/
builder.rs

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