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