Skip to main content

mago_codex/ttype/
expander.rs

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