mago_codex/ttype/
expander.rs

1use std::borrow::Cow;
2use std::cell::RefCell;
3
4use ahash::HashSet;
5use mago_atom::Atom;
6use mago_atom::ascii_lowercase_atom;
7
8use crate::identifier::function_like::FunctionLikeIdentifier;
9use crate::metadata::CodebaseMetadata;
10use crate::metadata::function_like::FunctionLikeMetadata;
11use crate::ttype::TType;
12use crate::ttype::atomic::TAtomic;
13use crate::ttype::atomic::alias::TAlias;
14use crate::ttype::atomic::array::TArray;
15use crate::ttype::atomic::callable::TCallable;
16use crate::ttype::atomic::callable::TCallableSignature;
17use crate::ttype::atomic::callable::parameter::TCallableParameter;
18use crate::ttype::atomic::derived::TDerived;
19use crate::ttype::atomic::derived::index_access::TIndexAccess;
20use crate::ttype::atomic::derived::int_mask::TIntMask;
21use crate::ttype::atomic::derived::int_mask_of::TIntMaskOf;
22use crate::ttype::atomic::derived::key_of::TKeyOf;
23use crate::ttype::atomic::derived::properties_of::TPropertiesOf;
24use crate::ttype::atomic::derived::value_of::TValueOf;
25use crate::ttype::atomic::mixed::TMixed;
26use crate::ttype::atomic::object::TObject;
27use crate::ttype::atomic::object::named::TNamedObject;
28use crate::ttype::atomic::reference::TReference;
29use crate::ttype::atomic::reference::TReferenceMemberSelector;
30use crate::ttype::atomic::scalar::TScalar;
31use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
32use crate::ttype::combiner;
33use crate::ttype::get_mixed;
34use crate::ttype::union::TUnion;
35
36thread_local! {
37    /// Thread-local set for tracking currently expanding aliases (cycle detection).
38    /// Uses a HashSet for accurate tracking without false positives from hash collisions.
39    static EXPANDING_ALIASES: RefCell<HashSet<(Atom, Atom)>> = const { RefCell::new(HashSet::with_hasher(ahash::RandomState::with_seeds(0, 0, 0, 0))) };
40
41    /// Thread-local set for tracking objects whose type parameters are being expanded (cycle detection).
42    static EXPANDING_OBJECT_PARAMS: RefCell<HashSet<Atom>> = const { RefCell::new(HashSet::with_hasher(ahash::RandomState::with_seeds(0, 0, 0, 0))) };
43}
44
45/// Resets the thread-local alias expansion state.
46///
47/// This is primarily useful for testing to ensure a clean state between tests.
48/// In normal usage, the RAII guards handle cleanup automatically.
49#[inline]
50pub fn reset_expansion_state() {
51    EXPANDING_ALIASES.with(|set| set.borrow_mut().clear());
52    EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().clear());
53}
54
55/// RAII guard to ensure alias expansion state is properly cleaned up.
56/// This guarantees the alias is removed from the set even if the expansion panics.
57struct AliasExpansionGuard {
58    class_name: Atom,
59    alias_name: Atom,
60}
61
62impl AliasExpansionGuard {
63    fn new(class_name: Atom, alias_name: Atom) -> Self {
64        EXPANDING_ALIASES.with(|set| set.borrow_mut().insert((class_name, alias_name)));
65        Self { class_name, alias_name }
66    }
67}
68
69impl Drop for AliasExpansionGuard {
70    fn drop(&mut self) {
71        EXPANDING_ALIASES.with(|set| set.borrow_mut().remove(&(self.class_name, self.alias_name)));
72    }
73}
74
75/// RAII guard for object type parameter expansion cycle detection.
76struct ObjectParamsExpansionGuard {
77    object_name: Atom,
78}
79
80impl ObjectParamsExpansionGuard {
81    fn try_new(object_name: Atom) -> Option<Self> {
82        EXPANDING_OBJECT_PARAMS.with(|set| {
83            let mut set = set.borrow_mut();
84            if set.contains(&object_name) {
85                None
86            } else {
87                set.insert(object_name);
88                Some(Self { object_name })
89            }
90        })
91    }
92}
93
94impl Drop for ObjectParamsExpansionGuard {
95    fn drop(&mut self) {
96        EXPANDING_OBJECT_PARAMS.with(|set| set.borrow_mut().remove(&self.object_name));
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Default)]
101pub enum StaticClassType {
102    #[default]
103    None,
104    Name(Atom),
105    Object(TObject),
106}
107
108#[derive(Debug)]
109pub struct TypeExpansionOptions {
110    pub self_class: Option<Atom>,
111    pub static_class_type: StaticClassType,
112    pub parent_class: Option<Atom>,
113    pub evaluate_class_constants: bool,
114    pub evaluate_conditional_types: bool,
115    pub function_is_final: bool,
116    pub expand_generic: bool,
117    pub expand_templates: bool,
118}
119
120impl Default for TypeExpansionOptions {
121    fn default() -> Self {
122        Self {
123            self_class: None,
124            static_class_type: StaticClassType::default(),
125            parent_class: None,
126            evaluate_class_constants: true,
127            evaluate_conditional_types: false,
128            function_is_final: false,
129            expand_generic: false,
130            expand_templates: true,
131        }
132    }
133}
134
135/// Expands a type union, resolving special types like `self`, `static`, `parent`,
136/// type aliases, class constants, and generic type parameters.
137pub fn expand_union(codebase: &CodebaseMetadata, return_type: &mut TUnion, options: &TypeExpansionOptions) {
138    if !return_type.is_expandable() {
139        return;
140    }
141
142    let mut types = std::mem::take(&mut return_type.types).into_owned();
143    let mut new_return_type_parts: Vec<TAtomic> = Vec::new();
144    let mut skip_mask: u64 = 0;
145
146    for (i, return_type_part) in types.iter_mut().enumerate() {
147        let mut skip_key = false;
148        expand_atomic(return_type_part, codebase, options, &mut skip_key, &mut new_return_type_parts);
149
150        if skip_key && i < 64 {
151            skip_mask |= 1u64 << i;
152        }
153    }
154
155    if skip_mask != 0 {
156        let mut idx = 0usize;
157        types.retain(|_| {
158            let retain = idx >= 64 || (skip_mask & (1u64 << idx)) == 0;
159            idx += 1;
160            retain
161        });
162
163        new_return_type_parts.append(&mut types);
164
165        if new_return_type_parts.is_empty() {
166            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
167        }
168
169        types = if new_return_type_parts.len() > 1 {
170            combiner::combine(new_return_type_parts, codebase, false)
171        } else {
172            new_return_type_parts
173        };
174    } else if types.len() > 1 {
175        types = combiner::combine(types, codebase, false);
176    }
177
178    return_type.types = Cow::Owned(types);
179}
180
181pub(crate) fn expand_atomic(
182    return_type_part: &mut TAtomic,
183    codebase: &CodebaseMetadata,
184    options: &TypeExpansionOptions,
185    skip_key: &mut bool,
186    new_return_type_parts: &mut Vec<TAtomic>,
187) {
188    match return_type_part {
189        TAtomic::Array(array_type) => match array_type {
190            TArray::Keyed(keyed_data) => {
191                if let Some((key_parameter, value_parameter)) = &mut keyed_data.parameters {
192                    expand_union(codebase, key_parameter, options);
193                    expand_union(codebase, value_parameter, options);
194                }
195
196                if let Some(known_items) = &mut keyed_data.known_items {
197                    for (_, item_type) in known_items.values_mut() {
198                        expand_union(codebase, item_type, options);
199                    }
200                }
201            }
202            TArray::List(list_data) => {
203                expand_union(codebase, &mut list_data.element_type, options);
204
205                if let Some(known_elements) = &mut list_data.known_elements {
206                    for (_, element_type) in known_elements.values_mut() {
207                        expand_union(codebase, element_type, options);
208                    }
209                }
210            }
211        },
212        TAtomic::Object(object) => {
213            expand_object(object, codebase, options);
214        }
215        TAtomic::Callable(TCallable::Signature(signature)) => {
216            if let Some(return_type) = signature.get_return_type_mut() {
217                expand_union(codebase, return_type, options);
218            }
219
220            for param in signature.get_parameters_mut() {
221                if let Some(param_type) = param.get_type_signature_mut() {
222                    expand_union(codebase, param_type, options);
223                }
224            }
225        }
226        TAtomic::GenericParameter(parameter) => {
227            expand_union(codebase, parameter.constraint.as_mut(), options);
228        }
229        TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) => {
230            let mut atomic_return_type_parts = vec![];
231            expand_atomic(constraint, codebase, options, &mut false, &mut atomic_return_type_parts);
232
233            if !atomic_return_type_parts.is_empty() {
234                **constraint = atomic_return_type_parts.remove(0);
235            }
236        }
237        TAtomic::Reference(TReference::Member { class_like_name, member_selector }) => {
238            *skip_key = true;
239            expand_member_reference(*class_like_name, member_selector, codebase, options, new_return_type_parts);
240        }
241        TAtomic::Callable(TCallable::Alias(id)) => {
242            if let Some(value) = get_atomic_of_function_like_identifier(id, codebase) {
243                *skip_key = true;
244                new_return_type_parts.push(value);
245            }
246        }
247        TAtomic::Conditional(conditional) => {
248            *skip_key = true;
249
250            let mut then = conditional.then.clone();
251            let mut otherwise = conditional.otherwise.clone();
252
253            expand_union(codebase, &mut then, options);
254            expand_union(codebase, &mut otherwise, options);
255
256            new_return_type_parts.extend(then.types.into_owned());
257            new_return_type_parts.extend(otherwise.types.into_owned());
258        }
259        TAtomic::Alias(alias) => {
260            *skip_key = true;
261            new_return_type_parts.extend(expand_alias(alias, codebase, options));
262        }
263        TAtomic::Derived(derived) => match derived {
264            TDerived::KeyOf(key_of) => {
265                *skip_key = true;
266                new_return_type_parts.extend(expand_key_of(key_of, codebase, options));
267            }
268            TDerived::ValueOf(value_of) => {
269                *skip_key = true;
270                new_return_type_parts.extend(expand_value_of(value_of, codebase, options));
271            }
272            TDerived::IndexAccess(index_access) => {
273                *skip_key = true;
274                new_return_type_parts.extend(expand_index_access(index_access, codebase, options));
275            }
276            TDerived::IntMask(int_mask) => {
277                *skip_key = true;
278                new_return_type_parts.extend(expand_int_mask(int_mask, codebase, options));
279            }
280            TDerived::IntMaskOf(int_mask_of) => {
281                *skip_key = true;
282                new_return_type_parts.extend(expand_int_mask_of(int_mask_of, codebase, options));
283            }
284            TDerived::PropertiesOf(properties_of) => {
285                *skip_key = true;
286                new_return_type_parts.extend(expand_properties_of(properties_of, codebase, options));
287            }
288        },
289        TAtomic::Iterable(iterable) => {
290            expand_union(codebase, &mut iterable.key_type, options);
291            expand_union(codebase, &mut iterable.value_type, options);
292        }
293        _ => {}
294    }
295}
296
297#[cold]
298fn expand_member_reference(
299    class_like_name: Atom,
300    member_selector: &TReferenceMemberSelector,
301    codebase: &CodebaseMetadata,
302    options: &TypeExpansionOptions,
303    new_return_type_parts: &mut Vec<TAtomic>,
304) {
305    let Some(class_like) = codebase.get_class_like(&class_like_name) else {
306        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
307        return;
308    };
309
310    for (constant_name, constant) in &class_like.constants {
311        if !member_selector.matches(constant_name) {
312            continue;
313        }
314
315        if let Some(inferred_type) = constant.inferred_type.as_ref() {
316            let mut inferred_type = inferred_type.clone();
317            let mut skip_inferred_type = false;
318            expand_atomic(&mut inferred_type, codebase, options, &mut skip_inferred_type, new_return_type_parts);
319
320            if !skip_inferred_type {
321                new_return_type_parts.push(inferred_type);
322            }
323        } else if let Some(type_metadata) = constant.type_metadata.as_ref() {
324            let mut constant_type = type_metadata.type_union.clone();
325            expand_union(codebase, &mut constant_type, options);
326            new_return_type_parts.extend(constant_type.types.into_owned());
327        } else {
328            new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
329        }
330    }
331
332    for enum_case_name in class_like.enum_cases.keys() {
333        if !member_selector.matches(enum_case_name) {
334            continue;
335        }
336        new_return_type_parts.push(TAtomic::Object(TObject::new_enum_case(class_like.original_name, *enum_case_name)));
337    }
338
339    if let TReferenceMemberSelector::Identifier(member_name) = member_selector
340        && let Some(type_alias) = class_like.type_aliases.get(member_name)
341    {
342        let mut alias_type = type_alias.type_union.clone();
343        expand_union(codebase, &mut alias_type, options);
344        new_return_type_parts.extend(alias_type.types.into_owned());
345    }
346
347    if new_return_type_parts.is_empty() {
348        new_return_type_parts.push(TAtomic::Mixed(TMixed::new()));
349    }
350}
351
352fn expand_object(named_object: &mut TObject, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) {
353    let Some(name) = named_object.get_name().copied() else {
354        return;
355    };
356
357    let is_this = if let TObject::Named(named_object) = named_object { named_object.is_this() } else { false };
358    let name_str_lc = ascii_lowercase_atom(&name);
359
360    if is_this || name_str_lc == "static" || name_str_lc == "$this" {
361        match &options.static_class_type {
362            StaticClassType::Object(TObject::Enum(static_enum)) => {
363                *named_object = TObject::Enum(static_enum.clone());
364            }
365            StaticClassType::Object(TObject::Named(static_object))
366                if name_str_lc == "static" || name_str_lc == "$this" =>
367            {
368                if let TObject::Named(named_object) = named_object {
369                    if let Some(static_object_intersections) = &static_object.intersection_types {
370                        let intersections = named_object.intersection_types.get_or_insert_with(Vec::new);
371                        intersections.extend(static_object_intersections.iter().cloned());
372                    }
373
374                    if named_object.type_parameters.is_none() {
375                        named_object.type_parameters.clone_from(&static_object.type_parameters);
376                    }
377
378                    named_object.name = static_object.name;
379                    named_object.is_this = true;
380                }
381            }
382            StaticClassType::Name(static_class_name)
383                if name_str_lc == "static"
384                    || name_str_lc == "$this"
385                    || (is_this && codebase.is_instance_of(static_class_name, &name)) =>
386            {
387                if let TObject::Named(named_object) = named_object {
388                    named_object.name = *static_class_name;
389                    named_object.is_this = false;
390                }
391            }
392            _ => {}
393        }
394    } else if name_str_lc == "self" {
395        if let Some(self_class_name) = options.self_class
396            && let TObject::Named(named_object) = named_object
397        {
398            named_object.name = self_class_name;
399        }
400    } else if name_str_lc == "parent"
401        && let Some(self_class_name) = options.self_class
402        && let Some(class_metadata) = codebase.get_class_like(&self_class_name)
403        && let Some(parent_name) = class_metadata.direct_parent_class
404        && let TObject::Named(named_object) = named_object
405    {
406        named_object.name = parent_name;
407    }
408
409    let TObject::Named(named_object) = named_object else {
410        return;
411    };
412
413    let Some(_guard) = ObjectParamsExpansionGuard::try_new(named_object.name) else {
414        return;
415    };
416
417    if let Some(class_like_metadata) = codebase.get_class_like(&named_object.name) {
418        for required_parent in &class_like_metadata.require_extends {
419            named_object.add_intersection_type(TAtomic::Object(TObject::Named(TNamedObject::new(*required_parent))));
420        }
421
422        for required_interface in &class_like_metadata.require_implements {
423            named_object.add_intersection_type(TAtomic::Object(TObject::Named(TNamedObject::new(*required_interface))));
424        }
425    }
426
427    match &mut named_object.type_parameters {
428        Some(type_parameters) if !type_parameters.is_empty() => {
429            for type_parameter in type_parameters.iter_mut() {
430                expand_union(codebase, type_parameter, options);
431            }
432        }
433        _ => {
434            if let Some(class_like_metadata) = codebase.get_class_like(&named_object.name)
435                && !class_like_metadata.template_types.is_empty()
436            {
437                let default_params: Vec<TUnion> = class_like_metadata
438                    .template_types
439                    .iter()
440                    .map(|(_, template_map)| {
441                        template_map.iter().map(|(_, t)| t).next().cloned().unwrap_or_else(get_mixed)
442                    })
443                    .collect();
444
445                if !default_params.is_empty() {
446                    named_object.type_parameters = Some(default_params);
447                }
448            }
449        }
450    }
451}
452
453#[must_use]
454pub fn get_signature_of_function_like_identifier(
455    function_like_identifier: &FunctionLikeIdentifier,
456    codebase: &CodebaseMetadata,
457) -> Option<TCallableSignature> {
458    Some(match function_like_identifier {
459        FunctionLikeIdentifier::Function(name) => {
460            let function_like_metadata = codebase.get_function(name)?;
461
462            get_signature_of_function_like_metadata(
463                function_like_identifier,
464                function_like_metadata,
465                codebase,
466                &TypeExpansionOptions::default(),
467            )
468        }
469        FunctionLikeIdentifier::Closure(file_id, position) => {
470            let function_like_metadata = codebase.get_closure(file_id, position)?;
471
472            get_signature_of_function_like_metadata(
473                function_like_identifier,
474                function_like_metadata,
475                codebase,
476                &TypeExpansionOptions::default(),
477            )
478        }
479        FunctionLikeIdentifier::Method(classlike_name, method_name) => {
480            let function_like_metadata = codebase.get_declaring_method(classlike_name, method_name)?;
481
482            get_signature_of_function_like_metadata(
483                function_like_identifier,
484                function_like_metadata,
485                codebase,
486                &TypeExpansionOptions {
487                    self_class: Some(*classlike_name),
488                    static_class_type: StaticClassType::Name(*classlike_name),
489                    ..Default::default()
490                },
491            )
492        }
493    })
494}
495
496#[must_use]
497pub fn get_atomic_of_function_like_identifier(
498    function_like_identifier: &FunctionLikeIdentifier,
499    codebase: &CodebaseMetadata,
500) -> Option<TAtomic> {
501    let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
502
503    Some(TAtomic::Callable(TCallable::Signature(signature)))
504}
505
506#[must_use]
507pub fn get_signature_of_function_like_metadata(
508    function_like_identifier: &FunctionLikeIdentifier,
509    function_like_metadata: &FunctionLikeMetadata,
510    codebase: &CodebaseMetadata,
511    options: &TypeExpansionOptions,
512) -> TCallableSignature {
513    let parameters: Vec<_> = function_like_metadata
514        .parameters
515        .iter()
516        .map(|parameter_metadata| {
517            let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
518                let mut t = t.type_union.clone();
519                expand_union(codebase, &mut t, options);
520                Some(Box::new(t))
521            } else {
522                None
523            };
524
525            TCallableParameter::new(
526                type_signature,
527                parameter_metadata.flags.is_by_reference(),
528                parameter_metadata.flags.is_variadic(),
529                parameter_metadata.flags.has_default(),
530            )
531        })
532        .collect();
533
534    let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
535        let mut return_type = type_metadata.type_union.clone();
536        expand_union(codebase, &mut return_type, options);
537        Some(Box::new(return_type))
538    } else {
539        None
540    };
541
542    let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), true)
543        .with_parameters(parameters)
544        .with_return_type(return_type)
545        .with_source(Some(*function_like_identifier));
546
547    if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
548        signature = signature.with_closure_location(Some((*file_id, *closure_position)));
549    }
550
551    signature
552}
553
554#[cold]
555fn expand_key_of(
556    return_type_key_of: &TKeyOf,
557    codebase: &CodebaseMetadata,
558    options: &TypeExpansionOptions,
559) -> Vec<TAtomic> {
560    let mut target_type = return_type_key_of.get_target_type().clone();
561    expand_union(codebase, &mut target_type, options);
562
563    let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
564        return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
565    };
566
567    new_return_types.types.into_owned()
568}
569
570#[cold]
571fn expand_value_of(
572    return_type_value_of: &TValueOf,
573    codebase: &CodebaseMetadata,
574    options: &TypeExpansionOptions,
575) -> Vec<TAtomic> {
576    let mut target_type = return_type_value_of.get_target_type().clone();
577    expand_union(codebase, &mut target_type, options);
578
579    let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
580        return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
581    };
582
583    new_return_types.types.into_owned()
584}
585
586#[cold]
587fn expand_index_access(
588    return_type_index_access: &TIndexAccess,
589    codebase: &CodebaseMetadata,
590    options: &TypeExpansionOptions,
591) -> Vec<TAtomic> {
592    let mut target_type = return_type_index_access.get_target_type().clone();
593    expand_union(codebase, &mut target_type, options);
594
595    let mut index_type = return_type_index_access.get_index_type().clone();
596    expand_union(codebase, &mut index_type, options);
597
598    let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
599    else {
600        return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
601    };
602
603    new_return_types.types.into_owned()
604}
605
606#[cold]
607fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
608    let mut literal_values = Vec::new();
609
610    for value in int_mask.get_values() {
611        let mut expanded = value.clone();
612        expand_union(codebase, &mut expanded, options);
613
614        if let Some(int_val) = expanded.get_single_literal_int_value() {
615            literal_values.push(int_val);
616        }
617    }
618
619    if literal_values.is_empty() {
620        return vec![TAtomic::Scalar(TScalar::int())];
621    }
622
623    let combinations = TIntMask::calculate_mask_combinations(&literal_values);
624    combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
625}
626
627#[cold]
628fn expand_int_mask_of(
629    int_mask_of: &TIntMaskOf,
630    codebase: &CodebaseMetadata,
631    options: &TypeExpansionOptions,
632) -> Vec<TAtomic> {
633    let mut target = int_mask_of.get_target_type().clone();
634    expand_union(codebase, &mut target, options);
635
636    let mut literal_values = Vec::new();
637    for atomic in target.types.iter() {
638        if let Some(int_val) = atomic.get_literal_int_value() {
639            literal_values.push(int_val);
640        }
641    }
642
643    if literal_values.is_empty() {
644        return vec![TAtomic::Scalar(TScalar::int())];
645    }
646
647    let combinations = TIntMask::calculate_mask_combinations(&literal_values);
648    combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
649}
650
651#[cold]
652fn expand_properties_of(
653    properties_of: &TPropertiesOf,
654    codebase: &CodebaseMetadata,
655    options: &TypeExpansionOptions,
656) -> Vec<TAtomic> {
657    let mut target_type = properties_of.get_target_type().clone();
658    expand_union(codebase, &mut target_type, options);
659
660    let Some(keyed_array) =
661        TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
662    else {
663        return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
664    };
665
666    vec![keyed_array]
667}
668
669#[cold]
670fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
671    let class_name = alias.get_class_name();
672    let alias_name = alias.get_alias_name();
673
674    // Check for cycle using the HashSet
675    let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
676
677    if is_cycle {
678        return vec![TAtomic::Alias(alias.clone())];
679    }
680
681    let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
682        return vec![TAtomic::Alias(alias.clone())];
683    };
684
685    let _ = AliasExpansionGuard::new(class_name, alias_name);
686
687    expand_union(codebase, &mut expanded_union, options);
688
689    expanded_union.types.into_owned()
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    use std::borrow::Cow;
697
698    use bumpalo::Bump;
699
700    use mago_atom::atom;
701    use mago_database::Database;
702    use mago_database::DatabaseReader;
703    use mago_database::file::File;
704    use mago_database::file::FileId;
705    use mago_names::resolver::NameResolver;
706    use mago_span::Position;
707    use mago_syntax::parser::parse_file;
708
709    use crate::metadata::CodebaseMetadata;
710    use crate::misc::GenericParent;
711    use crate::populator::populate_codebase;
712    use crate::reference::SymbolReferences;
713    use crate::scanner::scan_program;
714    use crate::ttype::atomic::array::TArray;
715    use crate::ttype::atomic::array::keyed::TKeyedArray;
716    use crate::ttype::atomic::array::list::TList;
717    use crate::ttype::atomic::callable::TCallable;
718    use crate::ttype::atomic::callable::TCallableSignature;
719    use crate::ttype::atomic::callable::parameter::TCallableParameter;
720    use crate::ttype::atomic::conditional::TConditional;
721    use crate::ttype::atomic::derived::TDerived;
722    use crate::ttype::atomic::derived::index_access::TIndexAccess;
723    use crate::ttype::atomic::derived::key_of::TKeyOf;
724    use crate::ttype::atomic::derived::value_of::TValueOf;
725    use crate::ttype::atomic::generic::TGenericParameter;
726    use crate::ttype::atomic::iterable::TIterable;
727    use crate::ttype::atomic::object::r#enum::TEnum;
728    use crate::ttype::atomic::object::named::TNamedObject;
729    use crate::ttype::atomic::reference::TReference;
730    use crate::ttype::atomic::reference::TReferenceMemberSelector;
731    use crate::ttype::atomic::scalar::TScalar;
732    use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
733    use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
734    use crate::ttype::get_int;
735    use crate::ttype::get_mixed;
736    use crate::ttype::get_never;
737    use crate::ttype::get_null;
738    use crate::ttype::get_string;
739    use crate::ttype::get_void;
740    use crate::ttype::union::UnionFlags;
741
742    fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
743        let file = File::ephemeral(Cow::Borrowed("code.php"), Cow::Borrowed(code));
744        let config =
745            mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
746                .into_static();
747        let database = Database::single(file, config);
748
749        let mut codebase = CodebaseMetadata::new();
750        let arena = Bump::new();
751        for file in database.files() {
752            let program = parse_file(&arena, &file).0;
753            let resolved_names = NameResolver::new(&arena).resolve(program);
754            let program_codebase = scan_program(&arena, &file, program, &resolved_names);
755
756            codebase.extend(program_codebase);
757        }
758
759        populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
760
761        codebase
762    }
763
764    fn options_with_self(self_class: &str) -> TypeExpansionOptions {
765        TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
766    }
767
768    fn options_with_static(static_class: &str) -> TypeExpansionOptions {
769        TypeExpansionOptions {
770            self_class: Some(ascii_lowercase_atom(static_class)),
771            static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
772            ..Default::default()
773        }
774    }
775
776    fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
777        let name = object.get_name().copied();
778        TypeExpansionOptions {
779            self_class: name,
780            static_class_type: StaticClassType::Object(object),
781            ..Default::default()
782        }
783    }
784
785    macro_rules! assert_expands_to {
786        ($codebase:expr, $input:expr, $expected:expr) => {
787            assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
788        };
789        ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
790            let mut actual = $input.clone();
791            expand_union($codebase, &mut actual, $options);
792            assert_eq!(
793                actual.types.as_ref(),
794                $expected.types.as_ref(),
795                "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
796                $input,
797                $expected,
798                actual
799            );
800        }};
801    }
802
803    fn make_self_object() -> TUnion {
804        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
805    }
806
807    fn make_static_object() -> TUnion {
808        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
809    }
810
811    fn make_parent_object() -> TUnion {
812        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
813    }
814
815    fn make_named_object(name: &str) -> TUnion {
816        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
817    }
818
819    #[test]
820    fn test_expand_null_type() {
821        let codebase = CodebaseMetadata::new();
822        let null_type = get_null();
823        assert_expands_to!(&codebase, null_type, get_null());
824    }
825
826    #[test]
827    fn test_expand_void_type() {
828        let codebase = CodebaseMetadata::new();
829        let void_type = get_void();
830        assert_expands_to!(&codebase, void_type, get_void());
831    }
832
833    #[test]
834    fn test_expand_never_type() {
835        let codebase = CodebaseMetadata::new();
836        let never_type = get_never();
837        assert_expands_to!(&codebase, never_type, get_never());
838    }
839
840    #[test]
841    fn test_expand_int_type() {
842        let codebase = CodebaseMetadata::new();
843        let int_type = get_int();
844        assert_expands_to!(&codebase, int_type, get_int());
845    }
846
847    #[test]
848    fn test_expand_mixed_type() {
849        let codebase = CodebaseMetadata::new();
850        let mixed_type = get_mixed();
851        assert_expands_to!(&codebase, mixed_type, get_mixed());
852    }
853
854    #[test]
855    fn test_expand_keyed_array_with_self_key() {
856        let code = r"<?php class Foo {}";
857        let codebase = create_test_codebase(code);
858
859        let mut keyed = TKeyedArray::new();
860        keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
861        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
862
863        let options = options_with_self("Foo");
864        let mut actual = input.clone();
865        expand_union(&codebase, &mut actual, &options);
866
867        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
868            && let Some((key, _)) = &keyed.parameters
869        {
870            assert!(key.types.iter().any(|t| {
871                if let TAtomic::Object(TObject::Named(named)) = t {
872                    named.name == ascii_lowercase_atom("foo")
873                } else {
874                    false
875                }
876            }));
877        }
878    }
879
880    #[test]
881    fn test_expand_keyed_array_with_self_value() {
882        let code = r"<?php class Foo {}";
883        let codebase = create_test_codebase(code);
884
885        let mut keyed = TKeyedArray::new();
886        keyed.parameters = Some((Box::new(get_string()), Box::new(make_self_object())));
887        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
888
889        let options = options_with_self("Foo");
890        let mut actual = input.clone();
891        expand_union(&codebase, &mut actual, &options);
892
893        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
894            && let Some((_, value)) = &keyed.parameters
895        {
896            assert!(value.types.iter().any(|t| {
897                if let TAtomic::Object(TObject::Named(named)) = t {
898                    named.name == ascii_lowercase_atom("foo")
899                } else {
900                    false
901                }
902            }));
903        }
904    }
905
906    #[test]
907    fn test_expand_keyed_array_known_items() {
908        let code = r"<?php class Foo {}";
909        let codebase = create_test_codebase(code);
910
911        use crate::ttype::atomic::array::key::ArrayKey;
912        use std::collections::BTreeMap;
913
914        let mut keyed = TKeyedArray::new();
915        let mut known_items = BTreeMap::new();
916        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
917        keyed.known_items = Some(known_items);
918        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
919
920        let options = options_with_self("Foo");
921        let mut actual = input.clone();
922        expand_union(&codebase, &mut actual, &options);
923
924        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
925            && let Some(items) = &keyed.known_items
926        {
927            let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
928            assert!(item_type.types.iter().any(|t| {
929                if let TAtomic::Object(TObject::Named(named)) = t {
930                    named.name == ascii_lowercase_atom("foo")
931                } else {
932                    false
933                }
934            }));
935        }
936    }
937
938    #[test]
939    fn test_expand_list_with_self_element() {
940        let code = r"<?php class Foo {}";
941        let codebase = create_test_codebase(code);
942
943        let list = TList::new(Box::new(make_self_object()));
944        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
945
946        let options = options_with_self("Foo");
947        let mut actual = input.clone();
948        expand_union(&codebase, &mut actual, &options);
949
950        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
951            assert!(list.element_type.types.iter().any(|t| {
952                if let TAtomic::Object(TObject::Named(named)) = t {
953                    named.name == ascii_lowercase_atom("foo")
954                } else {
955                    false
956                }
957            }));
958        }
959    }
960
961    #[test]
962    fn test_expand_list_known_elements() {
963        let code = r"<?php class Foo {}";
964        let codebase = create_test_codebase(code);
965
966        use std::collections::BTreeMap;
967
968        let mut list = TList::new(Box::new(get_mixed()));
969        let mut known_elements = BTreeMap::new();
970        known_elements.insert(0, (false, make_self_object()));
971        list.known_elements = Some(known_elements);
972        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
973
974        let options = options_with_self("Foo");
975        let mut actual = input.clone();
976        expand_union(&codebase, &mut actual, &options);
977
978        if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
979            && let Some(elements) = &list.known_elements
980        {
981            let (_, element_type) = elements.get(&0).unwrap();
982            assert!(element_type.types.iter().any(|t| {
983                if let TAtomic::Object(TObject::Named(named)) = t {
984                    named.name == ascii_lowercase_atom("foo")
985                } else {
986                    false
987                }
988            }));
989        }
990    }
991
992    #[test]
993    fn test_expand_nested_array() {
994        let code = r"<?php class Foo {}";
995        let codebase = create_test_codebase(code);
996
997        let inner_list = TList::new(Box::new(make_self_object()));
998        let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
999
1000        let mut outer = TKeyedArray::new();
1001        outer.parameters = Some((Box::new(make_self_object()), Box::new(inner_array)));
1002        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1003
1004        let options = options_with_self("Foo");
1005        let mut actual = input.clone();
1006        expand_union(&codebase, &mut actual, &options);
1007
1008        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1009            && let Some((key, value)) = &keyed.parameters
1010        {
1011            assert!(key.types.iter().any(|t| {
1012                if let TAtomic::Object(TObject::Named(named)) = t {
1013                    named.name == ascii_lowercase_atom("foo")
1014                } else {
1015                    false
1016                }
1017            }));
1018            if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1019                assert!(inner.element_type.types.iter().any(|t| {
1020                    if let TAtomic::Object(TObject::Named(named)) = t {
1021                        named.name == ascii_lowercase_atom("foo")
1022                    } else {
1023                        false
1024                    }
1025                }));
1026            }
1027        }
1028    }
1029
1030    #[test]
1031    fn test_expand_empty_array() {
1032        let codebase = CodebaseMetadata::new();
1033        let keyed = TKeyedArray::new();
1034        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1035        let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1036        assert_expands_to!(&codebase, input, expected);
1037    }
1038
1039    #[test]
1040    fn test_expand_non_empty_list() {
1041        let code = r"<?php class Foo {}";
1042        let codebase = create_test_codebase(code);
1043
1044        let mut list = TList::new(Box::new(make_self_object()));
1045        list.non_empty = true;
1046        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1047
1048        let options = options_with_self("Foo");
1049        let mut actual = input.clone();
1050        expand_union(&codebase, &mut actual, &options);
1051
1052        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1053            assert!(list.non_empty);
1054            assert!(list.element_type.types.iter().any(|t| {
1055                if let TAtomic::Object(TObject::Named(named)) = t {
1056                    named.name == ascii_lowercase_atom("foo")
1057                } else {
1058                    false
1059                }
1060            }));
1061        }
1062    }
1063
1064    #[test]
1065    fn test_expand_self_to_class_name() {
1066        let code = r"<?php class Foo {}";
1067        let codebase = create_test_codebase(code);
1068
1069        let input = make_self_object();
1070        let options = options_with_self("Foo");
1071        let mut actual = input.clone();
1072        expand_union(&codebase, &mut actual, &options);
1073
1074        assert!(actual.types.iter().any(|t| {
1075            if let TAtomic::Object(TObject::Named(named)) = t {
1076                named.name == ascii_lowercase_atom("foo")
1077            } else {
1078                false
1079            }
1080        }));
1081    }
1082
1083    #[test]
1084    fn test_expand_static_to_class_name() {
1085        let code = r"<?php class Foo {}";
1086        let codebase = create_test_codebase(code);
1087
1088        let input = make_static_object();
1089        let options = options_with_static("Foo");
1090        let mut actual = input.clone();
1091        expand_union(&codebase, &mut actual, &options);
1092
1093        assert!(actual.types.iter().any(|t| {
1094            if let TAtomic::Object(TObject::Named(named)) = t {
1095                named.name == ascii_lowercase_atom("foo")
1096            } else {
1097                false
1098            }
1099        }));
1100    }
1101
1102    #[test]
1103    fn test_expand_static_with_object_type() {
1104        let code = r"<?php class Foo {}";
1105        let codebase = create_test_codebase(code);
1106
1107        let input = make_static_object();
1108        let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1109        let options = options_with_static_object(static_obj);
1110        let mut actual = input.clone();
1111        expand_union(&codebase, &mut actual, &options);
1112
1113        assert!(actual.types.iter().any(|t| {
1114            if let TAtomic::Object(TObject::Named(named)) = t {
1115                named.name == ascii_lowercase_atom("foo") && named.is_this
1116            } else {
1117                false
1118            }
1119        }));
1120    }
1121
1122    #[test]
1123    fn test_expand_static_with_enum_type() {
1124        let code = r"<?php enum Status { case Active; case Inactive; }";
1125        let codebase = create_test_codebase(code);
1126
1127        let input = make_static_object();
1128        let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1129        let options = options_with_static_object(static_enum);
1130        let mut actual = input.clone();
1131        expand_union(&codebase, &mut actual, &options);
1132
1133        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1134    }
1135
1136    #[test]
1137    fn test_expand_parent_to_parent_class() {
1138        let code = r"<?php
1139            class BaseClass {}
1140            class ChildClass extends BaseClass {}
1141        ";
1142        let codebase = create_test_codebase(code);
1143
1144        let input = make_parent_object();
1145        let options = options_with_self("ChildClass");
1146        let mut actual = input.clone();
1147        expand_union(&codebase, &mut actual, &options);
1148
1149        assert!(actual.types.iter().any(|t| {
1150            if let TAtomic::Object(TObject::Named(named)) = t {
1151                named.name == ascii_lowercase_atom("baseclass")
1152            } else {
1153                false
1154            }
1155        }));
1156    }
1157
1158    #[test]
1159    fn test_expand_parent_without_parent_class() {
1160        let code = r"<?php class Foo {}";
1161        let codebase = create_test_codebase(code);
1162
1163        let input = make_parent_object();
1164        let options = options_with_self("Foo");
1165        let mut actual = input.clone();
1166        expand_union(&codebase, &mut actual, &options);
1167
1168        assert!(actual.types.iter().any(|t| {
1169            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1170        }));
1171    }
1172
1173    #[test]
1174    fn test_expand_this_variable() {
1175        let code = r"<?php class Foo {}";
1176        let codebase = create_test_codebase(code);
1177
1178        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1179        let options = options_with_static("Foo");
1180        let mut actual = input.clone();
1181        expand_union(&codebase, &mut actual, &options);
1182
1183        assert!(actual.types.iter().any(|t| {
1184            if let TAtomic::Object(TObject::Named(named)) = t {
1185                named.name == ascii_lowercase_atom("foo")
1186            } else {
1187                false
1188            }
1189        }));
1190    }
1191
1192    #[test]
1193    fn test_expand_this_with_final_function() {
1194        let code = r"<?php class Foo {}";
1195        let codebase = create_test_codebase(code);
1196
1197        let input = make_static_object();
1198        let options = TypeExpansionOptions {
1199            self_class: Some(ascii_lowercase_atom("foo")),
1200            static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1201            function_is_final: true,
1202            ..Default::default()
1203        };
1204        let mut actual = input.clone();
1205        expand_union(&codebase, &mut actual, &options);
1206
1207        assert!(actual.types.iter().any(|t| {
1208            if let TAtomic::Object(TObject::Named(named)) = t {
1209                named.name == ascii_lowercase_atom("foo") && !named.is_this
1210            } else {
1211                false
1212            }
1213        }));
1214    }
1215
1216    #[test]
1217    fn test_expand_object_with_type_parameters() {
1218        let code = r"<?php class Container {}";
1219        let codebase = create_test_codebase(code);
1220
1221        let named =
1222            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1223        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1224
1225        let options = options_with_self("Foo");
1226        let mut actual = input.clone();
1227        expand_union(&codebase, &mut actual, &options);
1228
1229        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1230            && let Some(params) = &named.type_parameters
1231        {
1232            assert!(params[0].types.iter().any(|t| {
1233                if let TAtomic::Object(TObject::Named(named)) = t {
1234                    named.name == ascii_lowercase_atom("foo")
1235                } else {
1236                    false
1237                }
1238            }));
1239        }
1240    }
1241
1242    #[test]
1243    fn test_expand_object_gets_default_type_params() {
1244        let code = r"<?php
1245            /** @template T */
1246            class Container {}
1247        ";
1248        let codebase = create_test_codebase(code);
1249
1250        let named = TNamedObject::new(ascii_lowercase_atom("container"));
1251        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1252
1253        let mut actual = input.clone();
1254        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1255
1256        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1257            assert!(named.type_parameters.is_some());
1258        }
1259    }
1260
1261    #[test]
1262    fn test_expand_object_intersection_from_static() {
1263        let code = r"<?php
1264            interface Stringable {}
1265            class Foo implements Stringable {}
1266        ";
1267        let codebase = create_test_codebase(code);
1268
1269        let input = make_static_object();
1270
1271        let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1272        static_named.intersection_types =
1273            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1274        let static_obj = TObject::Named(static_named);
1275        let options = options_with_static_object(static_obj);
1276
1277        let mut actual = input.clone();
1278        expand_union(&codebase, &mut actual, &options);
1279
1280        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1281            assert!(named.intersection_types.is_some());
1282        }
1283    }
1284
1285    #[test]
1286    fn test_expand_self_without_self_class_option() {
1287        let codebase = CodebaseMetadata::new();
1288
1289        let input = make_self_object();
1290        let mut actual = input.clone();
1291        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1292
1293        assert!(actual.types.iter().any(|t| {
1294            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1295        }));
1296    }
1297
1298    #[test]
1299    fn test_expand_callable_return_type() {
1300        let code = r"<?php class Foo {}";
1301        let codebase = create_test_codebase(code);
1302
1303        let sig = TCallableSignature::new(false, false).with_return_type(Some(Box::new(make_self_object())));
1304        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1305
1306        let options = options_with_self("Foo");
1307        let mut actual = input.clone();
1308        expand_union(&codebase, &mut actual, &options);
1309
1310        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1311            && let Some(ret) = sig.get_return_type()
1312        {
1313            assert!(ret.types.iter().any(|t| {
1314                if let TAtomic::Object(TObject::Named(named)) = t {
1315                    named.name == ascii_lowercase_atom("foo")
1316                } else {
1317                    false
1318                }
1319            }));
1320        }
1321    }
1322
1323    #[test]
1324    fn test_expand_callable_parameter_types() {
1325        let code = r"<?php class Foo {}";
1326        let codebase = create_test_codebase(code);
1327
1328        let param = TCallableParameter::new(Some(Box::new(make_self_object())), false, false, false);
1329        let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1330        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1331
1332        let options = options_with_self("Foo");
1333        let mut actual = input.clone();
1334        expand_union(&codebase, &mut actual, &options);
1335
1336        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1337            && let Some(param) = sig.get_parameters().first()
1338            && let Some(param_type) = param.get_type_signature()
1339        {
1340            assert!(param_type.types.iter().any(|t| {
1341                if let TAtomic::Object(TObject::Named(named)) = t {
1342                    named.name == ascii_lowercase_atom("foo")
1343                } else {
1344                    false
1345                }
1346            }));
1347        }
1348    }
1349
1350    #[test]
1351    fn test_expand_callable_alias_to_function() {
1352        let code = r"<?php
1353            function myFunc(): int { return 1; }
1354        ";
1355        let codebase = create_test_codebase(code);
1356
1357        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1358        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1359
1360        let mut actual = input.clone();
1361        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1362
1363        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1364    }
1365
1366    #[test]
1367    fn test_expand_callable_alias_to_method() {
1368        let code = r"<?php
1369            class Foo {
1370                public function bar(): int { return 1; }
1371            }
1372        ";
1373        let codebase = create_test_codebase(code);
1374
1375        let alias =
1376            TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1377        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1378
1379        let mut actual = input.clone();
1380        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1381
1382        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1383    }
1384
1385    #[test]
1386    fn test_expand_callable_alias_unknown() {
1387        let codebase = CodebaseMetadata::new();
1388
1389        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1390        let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1391
1392        let mut actual = input.clone();
1393        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1394
1395        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1396    }
1397
1398    #[test]
1399    fn test_expand_closure_signature() {
1400        let code = r"<?php class Foo {}";
1401        let codebase = create_test_codebase(code);
1402
1403        let sig = TCallableSignature::new(false, true).with_return_type(Some(Box::new(make_self_object())));
1404        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1405
1406        let options = options_with_self("Foo");
1407        let mut actual = input.clone();
1408        expand_union(&codebase, &mut actual, &options);
1409
1410        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1411            && let Some(ret) = sig.get_return_type()
1412        {
1413            assert!(ret.types.iter().any(|t| {
1414                if let TAtomic::Object(TObject::Named(named)) = t {
1415                    named.name == ascii_lowercase_atom("foo")
1416                } else {
1417                    false
1418                }
1419            }));
1420        }
1421    }
1422
1423    #[test]
1424    fn test_expand_generic_parameter_constraint() {
1425        let code = r"<?php class Foo {}";
1426        let codebase = create_test_codebase(code);
1427
1428        let generic = TGenericParameter::new(
1429            atom("T"),
1430            Box::new(make_self_object()),
1431            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1432        );
1433        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1434
1435        let options = options_with_self("Foo");
1436        let mut actual = input.clone();
1437        expand_union(&codebase, &mut actual, &options);
1438
1439        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1440            assert!(param.constraint.types.iter().any(|t| {
1441                if let TAtomic::Object(TObject::Named(named)) = t {
1442                    named.name == ascii_lowercase_atom("foo")
1443                } else {
1444                    false
1445                }
1446            }));
1447        }
1448    }
1449
1450    #[test]
1451    fn test_expand_nested_generic_constraint() {
1452        let code = r"<?php class Foo {} class Bar {}";
1453        let codebase = create_test_codebase(code);
1454
1455        let container =
1456            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1457        let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1458
1459        let generic = TGenericParameter::new(
1460            atom("T"),
1461            Box::new(constraint),
1462            GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1463        );
1464        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1465
1466        let options = options_with_self("Foo");
1467        let mut actual = input.clone();
1468        expand_union(&codebase, &mut actual, &options);
1469
1470        if let TAtomic::GenericParameter(param) = &actual.types[0]
1471            && let TAtomic::Object(TObject::Named(named)) = &param.constraint.types[0]
1472            && let Some(params) = &named.type_parameters
1473        {
1474            assert!(params[0].types.iter().any(|t| {
1475                if let TAtomic::Object(TObject::Named(named)) = t {
1476                    named.name == ascii_lowercase_atom("foo")
1477                } else {
1478                    false
1479                }
1480            }));
1481        }
1482    }
1483
1484    #[test]
1485    fn test_expand_generic_with_intersection() {
1486        let code = r"<?php
1487            interface Stringable {}
1488            class Foo {}
1489        ";
1490        let codebase = create_test_codebase(code);
1491
1492        let mut generic = TGenericParameter::new(
1493            atom("T"),
1494            Box::new(make_self_object()),
1495            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1496        );
1497        generic.intersection_types =
1498            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1499        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1500
1501        let options = options_with_self("Foo");
1502        let mut actual = input.clone();
1503        expand_union(&codebase, &mut actual, &options);
1504
1505        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1506            assert!(param.intersection_types.is_some());
1507            assert!(param.constraint.types.iter().any(|t| {
1508                if let TAtomic::Object(TObject::Named(named)) = t {
1509                    named.name == ascii_lowercase_atom("foo")
1510                } else {
1511                    false
1512                }
1513            }));
1514        }
1515    }
1516
1517    #[test]
1518    fn test_expand_class_string_of_self() {
1519        let code = r"<?php class Foo {}";
1520        let codebase = create_test_codebase(code);
1521
1522        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1523        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1524        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1525
1526        let options = options_with_self("Foo");
1527        let mut actual = input.clone();
1528        expand_union(&codebase, &mut actual, &options);
1529
1530        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1531            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1532        {
1533            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1534        }
1535    }
1536
1537    #[test]
1538    fn test_expand_class_string_of_static() {
1539        let code = r"<?php class Foo {}";
1540        let codebase = create_test_codebase(code);
1541
1542        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1543        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1544        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1545
1546        let options = options_with_static("Foo");
1547        let mut actual = input.clone();
1548        expand_union(&codebase, &mut actual, &options);
1549
1550        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1551            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1552        {
1553            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1554        }
1555    }
1556
1557    #[test]
1558    fn test_expand_interface_string_of_type() {
1559        let code = r"<?php interface MyInterface {}";
1560        let codebase = create_test_codebase(code);
1561
1562        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1563        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1564        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1565
1566        let options = options_with_self("MyInterface");
1567        let mut actual = input.clone();
1568        expand_union(&codebase, &mut actual, &options);
1569
1570        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1571            &actual.types[0]
1572        {
1573            assert!(matches!(kind, TClassLikeStringKind::Interface));
1574            if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1575                assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1576            }
1577        }
1578    }
1579
1580    #[test]
1581    fn test_expand_member_reference_wildcard_constants() {
1582        let code = r"<?php
1583            class Foo {
1584                public const A = 1;
1585                public const B = 2;
1586            }
1587        ";
1588        let codebase = create_test_codebase(code);
1589
1590        let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1591        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1592
1593        let mut actual = input.clone();
1594        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1595
1596        assert!(!actual.types.is_empty());
1597    }
1598
1599    #[test]
1600    fn test_expand_member_reference_wildcard_enum_cases() {
1601        let code = r"<?php
1602            enum Status {
1603                case Active;
1604                case Inactive;
1605            }
1606        ";
1607        let codebase = create_test_codebase(code);
1608
1609        let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1610        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1611
1612        let mut actual = input.clone();
1613        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1614
1615        assert_eq!(actual.types.len(), 2);
1616        assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1617    }
1618
1619    #[test]
1620    fn test_expand_member_reference_starts_with() {
1621        let code = r"<?php
1622            class Foo {
1623                public const STATUS_ACTIVE = 1;
1624                public const STATUS_INACTIVE = 2;
1625                public const OTHER = 3;
1626            }
1627        ";
1628        let codebase = create_test_codebase(code);
1629
1630        let reference =
1631            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
1632        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1633
1634        let mut actual = input.clone();
1635        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1636
1637        assert!(!actual.types.is_empty());
1638    }
1639
1640    #[test]
1641    fn test_expand_member_reference_ends_with() {
1642        let code = r"<?php
1643            class Foo {
1644                public const READ_ERROR = 1;
1645                public const WRITE_ERROR = 2;
1646                public const SUCCESS = 0;
1647            }
1648        ";
1649        let codebase = create_test_codebase(code);
1650
1651        let reference =
1652            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
1653        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1654
1655        let mut actual = input.clone();
1656        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1657
1658        assert!(!actual.types.is_empty());
1659    }
1660
1661    #[test]
1662    fn test_expand_member_reference_identifier_constant() {
1663        let code = r"<?php
1664            class Foo {
1665                public const BAR = 42;
1666            }
1667        ";
1668        let codebase = create_test_codebase(code);
1669
1670        let reference =
1671            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
1672        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1673
1674        let mut actual = input.clone();
1675        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1676
1677        assert_eq!(actual.types.len(), 1);
1678    }
1679
1680    #[test]
1681    fn test_expand_member_reference_identifier_enum_case() {
1682        let code = r"<?php
1683            enum Status {
1684                case Active;
1685            }
1686        ";
1687        let codebase = create_test_codebase(code);
1688
1689        let reference = TReference::new_member(
1690            ascii_lowercase_atom("status"),
1691            TReferenceMemberSelector::Identifier(atom("Active")),
1692        );
1693        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1694
1695        let mut actual = input.clone();
1696        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1697
1698        assert_eq!(actual.types.len(), 1);
1699        assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1700    }
1701
1702    #[test]
1703    fn test_expand_member_reference_unknown_class() {
1704        let codebase = CodebaseMetadata::new();
1705
1706        let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1707        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1708
1709        let mut actual = input.clone();
1710        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1711
1712        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1713    }
1714
1715    #[test]
1716    fn test_expand_member_reference_unknown_member() {
1717        let code = r"<?php class Foo {}";
1718        let codebase = create_test_codebase(code);
1719
1720        let reference = TReference::new_member(
1721            ascii_lowercase_atom("foo"),
1722            TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1723        );
1724        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1725
1726        let mut actual = input.clone();
1727        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1728
1729        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1730    }
1731
1732    #[test]
1733    fn test_expand_member_reference_constant_with_inferred_type() {
1734        let code = r#"<?php
1735            class Foo {
1736                public const VALUE = "hello";
1737            }
1738        "#;
1739        let codebase = create_test_codebase(code);
1740
1741        let reference =
1742            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1743        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1744
1745        let mut actual = input.clone();
1746        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1747
1748        assert_eq!(actual.types.len(), 1);
1749    }
1750
1751    #[test]
1752    fn test_expand_member_reference_constant_with_type_metadata() {
1753        let code = r"<?php
1754            class Foo {
1755                /** @var int */
1756                public const VALUE = 42;
1757            }
1758        ";
1759        let codebase = create_test_codebase(code);
1760
1761        let reference =
1762            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1763        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1764
1765        let mut actual = input.clone();
1766        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1767
1768        assert_eq!(actual.types.len(), 1);
1769    }
1770
1771    #[test]
1772    fn test_expand_conditional_both_branches() {
1773        let code = r"<?php class Foo {} class Bar {}";
1774        let codebase = create_test_codebase(code);
1775
1776        let conditional = TConditional::new(
1777            Box::new(get_mixed()),
1778            Box::new(get_string()),
1779            Box::new(make_self_object()),
1780            Box::new(make_self_object()),
1781            false,
1782        );
1783        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1784
1785        let options = options_with_self("Foo");
1786        let mut actual = input.clone();
1787        expand_union(&codebase, &mut actual, &options);
1788
1789        assert!(actual.types.iter().any(|t| {
1790            if let TAtomic::Object(TObject::Named(named)) = t {
1791                named.name == ascii_lowercase_atom("foo")
1792            } else {
1793                false
1794            }
1795        }));
1796    }
1797
1798    #[test]
1799    fn test_expand_conditional_with_self_in_then() {
1800        let code = r"<?php class Foo {}";
1801        let codebase = create_test_codebase(code);
1802
1803        let conditional = TConditional::new(
1804            Box::new(get_mixed()),
1805            Box::new(get_string()),
1806            Box::new(make_self_object()),
1807            Box::new(get_int()),
1808            false,
1809        );
1810        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1811
1812        let options = options_with_self("Foo");
1813        let mut actual = input.clone();
1814        expand_union(&codebase, &mut actual, &options);
1815
1816        assert!(!actual.types.is_empty());
1817    }
1818
1819    #[test]
1820    fn test_expand_conditional_with_self_in_otherwise() {
1821        let code = r"<?php class Foo {}";
1822        let codebase = create_test_codebase(code);
1823
1824        let conditional = TConditional::new(
1825            Box::new(get_mixed()),
1826            Box::new(get_string()),
1827            Box::new(get_int()),
1828            Box::new(make_self_object()),
1829            false,
1830        );
1831        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1832
1833        let options = options_with_self("Foo");
1834        let mut actual = input.clone();
1835        expand_union(&codebase, &mut actual, &options);
1836
1837        assert!(!actual.types.is_empty());
1838    }
1839
1840    #[test]
1841    fn test_expand_simple_alias() {
1842        let code = r"<?php
1843            class Foo {
1844                /** @phpstan-type MyInt = int */
1845            }
1846        ";
1847        let codebase = create_test_codebase(code);
1848
1849        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
1850        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1851
1852        let mut actual = input.clone();
1853        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1854
1855        assert!(!actual.types.is_empty());
1856    }
1857
1858    #[test]
1859    fn test_expand_nested_alias() {
1860        let code = r"<?php
1861            class Foo {
1862                /** @phpstan-type Inner = int */
1863                /** @phpstan-type Outer = Inner */
1864            }
1865        ";
1866        let codebase = create_test_codebase(code);
1867
1868        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
1869        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1870
1871        let mut actual = input.clone();
1872        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1873
1874        assert!(!actual.types.is_empty());
1875    }
1876
1877    #[test]
1878    fn test_expand_alias_cycle_detection() {
1879        let codebase = CodebaseMetadata::new();
1880
1881        let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
1882        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1883
1884        let mut actual = input.clone();
1885        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1886
1887        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1888    }
1889
1890    #[test]
1891    fn test_expand_alias_unknown() {
1892        let codebase = CodebaseMetadata::new();
1893
1894        let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
1895        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1896
1897        let mut actual = input.clone();
1898        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1899
1900        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1901    }
1902
1903    #[test]
1904    fn test_expand_alias_with_self_inside() {
1905        let code = r"<?php
1906            class Foo {
1907                /** @phpstan-type MySelf = self */
1908            }
1909        ";
1910        let codebase = create_test_codebase(code);
1911
1912        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
1913        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1914
1915        let options = options_with_self("Foo");
1916        let mut actual = input.clone();
1917        expand_union(&codebase, &mut actual, &options);
1918
1919        assert!(!actual.types.is_empty());
1920    }
1921
1922    #[test]
1923    fn test_expand_key_of_array() {
1924        let codebase = CodebaseMetadata::new();
1925
1926        let mut keyed = TKeyedArray::new();
1927        keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
1928        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1929
1930        let key_of = TKeyOf::new(Box::new(array_type));
1931        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
1932
1933        let mut actual = input.clone();
1934        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1935
1936        assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
1937    }
1938
1939    #[test]
1940    fn test_expand_key_of_with_self() {
1941        let code = r"<?php class Foo {}";
1942        let codebase = create_test_codebase(code);
1943
1944        let mut keyed = TKeyedArray::new();
1945        keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
1946        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1947
1948        let key_of = TKeyOf::new(Box::new(array_type));
1949        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
1950
1951        let options = options_with_self("Foo");
1952        let mut actual = input.clone();
1953        expand_union(&codebase, &mut actual, &options);
1954
1955        assert!(!actual.types.is_empty());
1956    }
1957
1958    #[test]
1959    fn test_expand_value_of_array() {
1960        let codebase = CodebaseMetadata::new();
1961
1962        let mut keyed = TKeyedArray::new();
1963        keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
1964        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1965
1966        let value_of = TValueOf::new(Box::new(array_type));
1967        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
1968
1969        let mut actual = input.clone();
1970        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1971
1972        assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
1973    }
1974
1975    #[test]
1976    fn test_expand_value_of_enum() {
1977        let code = r"<?php
1978            enum Status: string {
1979                case Active = 'active';
1980                case Inactive = 'inactive';
1981            }
1982        ";
1983        let codebase = create_test_codebase(code);
1984
1985        let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
1986
1987        let value_of = TValueOf::new(Box::new(enum_type));
1988        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
1989
1990        let mut actual = input.clone();
1991        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1992
1993        assert!(!actual.types.is_empty());
1994    }
1995
1996    #[test]
1997    fn test_expand_index_access() {
1998        let codebase = CodebaseMetadata::new();
1999
2000        use crate::ttype::atomic::array::key::ArrayKey;
2001        use std::collections::BTreeMap;
2002
2003        let mut keyed = TKeyedArray::new();
2004        let mut known_items = BTreeMap::new();
2005        known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2006        keyed.known_items = Some(known_items);
2007        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2008
2009        use crate::ttype::get_literal_string;
2010        let index_type = get_literal_string(atom("key"));
2011
2012        let index_access = TIndexAccess::new(array_type, index_type);
2013        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2014
2015        let mut actual = input.clone();
2016        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2017
2018        assert!(!actual.types.is_empty());
2019    }
2020
2021    #[test]
2022    fn test_expand_index_access_with_self() {
2023        let code = r"<?php class Foo {}";
2024        let codebase = create_test_codebase(code);
2025
2026        use crate::ttype::atomic::array::key::ArrayKey;
2027        use std::collections::BTreeMap;
2028
2029        let mut keyed = TKeyedArray::new();
2030        let mut known_items = BTreeMap::new();
2031        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2032        keyed.known_items = Some(known_items);
2033        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2034
2035        use crate::ttype::get_literal_string;
2036        let index_type = get_literal_string(atom("key"));
2037
2038        let index_access = TIndexAccess::new(array_type, index_type);
2039        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2040
2041        let options = options_with_self("Foo");
2042        let mut actual = input.clone();
2043        expand_union(&codebase, &mut actual, &options);
2044
2045        assert!(!actual.types.is_empty());
2046    }
2047
2048    #[test]
2049    fn test_expand_iterable_key_type() {
2050        let code = r"<?php class Foo {}";
2051        let codebase = create_test_codebase(code);
2052
2053        let iterable = TIterable::new(Box::new(make_self_object()), Box::new(get_int()));
2054        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2055
2056        let options = options_with_self("Foo");
2057        let mut actual = input.clone();
2058        expand_union(&codebase, &mut actual, &options);
2059
2060        if let TAtomic::Iterable(iter) = &actual.types[0] {
2061            assert!(iter.get_key_type().types.iter().any(|t| {
2062                if let TAtomic::Object(TObject::Named(named)) = t {
2063                    named.name == ascii_lowercase_atom("foo")
2064                } else {
2065                    false
2066                }
2067            }));
2068        }
2069    }
2070
2071    #[test]
2072    fn test_expand_iterable_value_type() {
2073        let code = r"<?php class Foo {}";
2074        let codebase = create_test_codebase(code);
2075
2076        let iterable = TIterable::new(Box::new(get_int()), Box::new(make_self_object()));
2077        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2078
2079        let options = options_with_self("Foo");
2080        let mut actual = input.clone();
2081        expand_union(&codebase, &mut actual, &options);
2082
2083        if let TAtomic::Iterable(iter) = &actual.types[0] {
2084            assert!(iter.get_value_type().types.iter().any(|t| {
2085                if let TAtomic::Object(TObject::Named(named)) = t {
2086                    named.name == ascii_lowercase_atom("foo")
2087                } else {
2088                    false
2089                }
2090            }));
2091        }
2092    }
2093
2094    #[test]
2095    fn test_get_signature_of_function() {
2096        let code = r#"<?php
2097            function myFunc(int $a): string { return ""; }
2098        "#;
2099        let codebase = create_test_codebase(code);
2100
2101        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2102
2103        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2104        assert!(sig.is_some());
2105
2106        let sig = sig.unwrap();
2107        assert_eq!(sig.get_parameters().len(), 1);
2108        assert!(sig.get_return_type().is_some());
2109    }
2110
2111    #[test]
2112    fn test_get_signature_of_method() {
2113        let code = r"<?php
2114            class Foo {
2115                public function bar(string $s): int { return 0; }
2116            }
2117        ";
2118        let codebase = create_test_codebase(code);
2119
2120        let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2121
2122        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2123        assert!(sig.is_some());
2124
2125        let sig = sig.unwrap();
2126        assert_eq!(sig.get_parameters().len(), 1);
2127    }
2128
2129    #[test]
2130    fn test_get_signature_of_closure() {
2131        let codebase = CodebaseMetadata::new();
2132
2133        let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2134        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2135
2136        assert!(sig.is_none());
2137    }
2138
2139    #[test]
2140    fn test_get_atomic_of_function() {
2141        let code = r"<?php
2142            function myFunc(): void {}
2143        ";
2144        let codebase = create_test_codebase(code);
2145
2146        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2147
2148        let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2149        assert!(atomic.is_some());
2150        assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2151    }
2152
2153    #[test]
2154    fn test_get_signature_with_parameters() {
2155        let code = r"<?php
2156            function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2157        ";
2158        let codebase = create_test_codebase(code);
2159
2160        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2161
2162        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2163        assert!(sig.is_some());
2164
2165        let sig = sig.unwrap();
2166        assert_eq!(sig.get_parameters().len(), 3);
2167
2168        let third_param = &sig.get_parameters()[2];
2169        assert!(third_param.has_default());
2170    }
2171
2172    #[test]
2173    fn test_expand_preserves_by_reference_flag() {
2174        let code = r"<?php class Foo {}";
2175        let codebase = create_test_codebase(code);
2176
2177        let mut input = make_self_object();
2178        input.flags.insert(UnionFlags::BY_REFERENCE);
2179
2180        let options = options_with_self("Foo");
2181        let mut actual = input.clone();
2182        expand_union(&codebase, &mut actual, &options);
2183
2184        assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2185    }
2186
2187    #[test]
2188    fn test_expand_preserves_possibly_undefined_flag() {
2189        let code = r"<?php class Foo {}";
2190        let codebase = create_test_codebase(code);
2191
2192        let mut input = make_self_object();
2193        input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2194
2195        let options = options_with_self("Foo");
2196        let mut actual = input.clone();
2197        expand_union(&codebase, &mut actual, &options);
2198
2199        assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2200    }
2201
2202    #[test]
2203    fn test_expand_multiple_self_in_union() {
2204        let code = r"<?php class Foo {}";
2205        let codebase = create_test_codebase(code);
2206
2207        let input = TUnion::from_vec(vec![
2208            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2209            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2210        ]);
2211
2212        let options = options_with_self("Foo");
2213        let mut actual = input.clone();
2214        expand_union(&codebase, &mut actual, &options);
2215
2216        assert!(actual.types.len() <= 2);
2217    }
2218
2219    #[test]
2220    fn test_expand_deeply_nested_types() {
2221        let code = r"<?php class Foo {}";
2222        let codebase = create_test_codebase(code);
2223
2224        let inner = TList::new(Box::new(make_self_object()));
2225        let middle = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2226        let outer = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2227        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2228
2229        let options = options_with_self("Foo");
2230        let mut actual = input.clone();
2231        expand_union(&codebase, &mut actual, &options);
2232
2233        if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2234            && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2235            && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2236        {
2237            assert!(inner.element_type.types.iter().any(|t| {
2238                if let TAtomic::Object(TObject::Named(named)) = t {
2239                    named.name == ascii_lowercase_atom("foo")
2240                } else {
2241                    false
2242                }
2243            }));
2244        }
2245    }
2246
2247    #[test]
2248    fn test_expand_with_all_options_disabled() {
2249        let code = r"<?php class Foo {}";
2250        let codebase = create_test_codebase(code);
2251
2252        let input = make_self_object();
2253        let options = TypeExpansionOptions {
2254            self_class: None,
2255            static_class_type: StaticClassType::None,
2256            parent_class: None,
2257            evaluate_class_constants: false,
2258            evaluate_conditional_types: false,
2259            function_is_final: false,
2260            expand_generic: false,
2261            expand_templates: false,
2262        };
2263
2264        let mut actual = input.clone();
2265        expand_union(&codebase, &mut actual, &options);
2266
2267        assert!(actual.types.iter().any(|t| {
2268            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2269        }));
2270    }
2271
2272    #[test]
2273    fn test_expand_already_expanded_type() {
2274        let code = r"<?php class Foo {}";
2275        let codebase = create_test_codebase(code);
2276
2277        let input = make_named_object("Foo");
2278        let options = options_with_self("Foo");
2279
2280        let mut actual = input.clone();
2281        expand_union(&codebase, &mut actual, &options);
2282
2283        let mut actual2 = actual.clone();
2284        expand_union(&codebase, &mut actual2, &options);
2285
2286        assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2287    }
2288
2289    #[test]
2290    fn test_expand_complex_generic_class() {
2291        let code = r"<?php
2292            /**
2293             * @template T
2294             * @template U
2295             */
2296            class Container {}
2297        ";
2298        let codebase = create_test_codebase(code);
2299
2300        let named = TNamedObject::new_with_type_parameters(
2301            ascii_lowercase_atom("container"),
2302            Some(vec![make_self_object(), make_static_object()]),
2303        );
2304        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2305
2306        let options = TypeExpansionOptions {
2307            self_class: Some(ascii_lowercase_atom("foo")),
2308            static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2309            ..Default::default()
2310        };
2311
2312        let mut actual = input.clone();
2313        expand_union(&codebase, &mut actual, &options);
2314
2315        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2316            && let Some(params) = &named.type_parameters
2317        {
2318            assert!(params[0].types.iter().any(|t| {
2319                if let TAtomic::Object(TObject::Named(named)) = t {
2320                    named.name == ascii_lowercase_atom("foo")
2321                } else {
2322                    false
2323                }
2324            }));
2325            assert!(params[1].types.iter().any(|t| {
2326                if let TAtomic::Object(TObject::Named(named)) = t {
2327                    named.name == ascii_lowercase_atom("bar")
2328                } else {
2329                    false
2330                }
2331            }));
2332        }
2333    }
2334}