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