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