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