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