Skip to main content

mago_codex/ttype/
expander.rs

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