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 was_this = named.is_this;
485
486    match name_lc.as_str() {
487        "static" | "$this" => resolve_static_type(named, was_this, 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 named.is_static => resolve_static_type(named, was_this, 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///
508/// `is_this_type`: true when the original type was `$this` (same instance), false for `static`.
509/// `check_compatibility`: when true, verifies the static type is compatible before resolving.
510fn resolve_static_type(
511    named: &mut TNamedObject,
512    is_this_type: bool,
513    check_compatibility: bool,
514    codebase: &CodebaseMetadata,
515    options: &TypeExpansionOptions,
516) {
517    match &options.static_class_type {
518        StaticClassType::Object(TObject::Named(static_obj)) => {
519            if check_compatibility && !is_static_type_compatible(named, static_obj, codebase) {
520                return;
521            }
522
523            if let Some(intersections) = &static_obj.intersection_types {
524                named.intersection_types.get_or_insert_with(Vec::new).extend(intersections.iter().cloned());
525            }
526
527            if static_obj.type_parameters.is_some() && should_use_static_type_params(named, static_obj, codebase) {
528                named.type_parameters.clone_from(&static_obj.type_parameters);
529            }
530
531            named.name = static_obj.name;
532            let effectively_final = is_effectively_final(&static_obj.name, codebase, options);
533            named.is_static = !effectively_final;
534            named.is_this = !effectively_final && is_this_type;
535        }
536        StaticClassType::Name(static_class)
537            if (!check_compatibility || codebase.is_instance_of(static_class, &named.name)) =>
538        {
539            named.name = *static_class;
540            let effectively_final = is_effectively_final(static_class, codebase, options);
541            named.is_static = !effectively_final;
542            named.is_this = !effectively_final && is_this_type;
543        }
544        _ => {}
545    }
546}
547
548/// Checks whether a class is effectively final for the purpose of `$this`/`static` resolution.
549///
550/// A class is effectively final when it cannot be extended, meaning `static` === `self`:
551///
552/// - The class is declared `final`
553/// - The class is anonymous
554/// - The method is declared `final`
555fn is_effectively_final(class_name: &Atom, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> bool {
556    if options.function_is_final {
557        return true;
558    }
559
560    codebase.get_class_like(class_name).is_some_and(|meta| meta.name_span.is_none() || meta.flags.is_final())
561}
562
563/// Checks if the static object type is compatible with a type that has is_this=true.
564fn is_static_type_compatible(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
565    codebase.is_instance_of(&static_obj.name, &named.name)
566        || static_obj
567            .intersection_types
568            .iter()
569            .flatten()
570            .filter_map(|t| if let TAtomic::Object(obj) = t { obj.get_name() } else { None })
571            .any(|name| codebase.is_instance_of(&name, &named.name))
572}
573
574/// Returns true if we should use the static object's type parameters instead of the current ones.
575/// This is true when current params are None or match the class's default template bounds.
576fn should_use_static_type_params(named: &TNamedObject, static_obj: &TNamedObject, codebase: &CodebaseMetadata) -> bool {
577    let Some(current_params) = &named.type_parameters else {
578        return true;
579    };
580
581    let Some(class_metadata) = codebase.get_class_like(&static_obj.name) else {
582        return false;
583    };
584
585    let templates = &class_metadata.template_types;
586
587    current_params.len() == templates.len()
588        && current_params.iter().zip(templates.values()).all(|(current, template)| current == &template.constraint)
589}
590
591/// Expands existing type parameters or fills them with default template bounds.
592fn expand_or_fill_type_parameters(
593    named: &mut TNamedObject,
594    codebase: &CodebaseMetadata,
595    options: &TypeExpansionOptions,
596) {
597    if let Some(params) = &mut named.type_parameters
598        && !params.is_empty()
599    {
600        for param in params.iter_mut() {
601            expand_union(codebase, param, options);
602        }
603        return;
604    }
605
606    let Some(class_metadata) = codebase.get_class_like(&named.name) else {
607        return;
608    };
609
610    if class_metadata.template_types.is_empty() {
611        return;
612    }
613
614    let defaults: Vec<TUnion> =
615        class_metadata.template_types.values().map(|template| template.constraint.clone()).collect();
616
617    named.type_parameters = Some(defaults);
618}
619
620#[must_use]
621pub fn get_signature_of_function_like_identifier(
622    function_like_identifier: &FunctionLikeIdentifier,
623    codebase: &CodebaseMetadata,
624) -> Option<TCallableSignature> {
625    Some(match function_like_identifier {
626        FunctionLikeIdentifier::Function(name) => {
627            let function_like_metadata = codebase.get_function(name)?;
628
629            get_signature_of_function_like_metadata(
630                function_like_identifier,
631                function_like_metadata,
632                codebase,
633                &TypeExpansionOptions::default(),
634            )
635        }
636        FunctionLikeIdentifier::Closure(file_id, position) => {
637            let function_like_metadata = codebase.get_closure(file_id, position)?;
638
639            get_signature_of_function_like_metadata(
640                function_like_identifier,
641                function_like_metadata,
642                codebase,
643                &TypeExpansionOptions::default(),
644            )
645        }
646        FunctionLikeIdentifier::Method(classlike_name, method_name) => {
647            let function_like_metadata = codebase.get_declaring_method(classlike_name, method_name)?;
648
649            get_signature_of_function_like_metadata(
650                function_like_identifier,
651                function_like_metadata,
652                codebase,
653                &TypeExpansionOptions {
654                    self_class: Some(*classlike_name),
655                    static_class_type: StaticClassType::Name(*classlike_name),
656                    ..Default::default()
657                },
658            )
659        }
660    })
661}
662
663#[must_use]
664pub fn get_atomic_of_function_like_identifier(
665    function_like_identifier: &FunctionLikeIdentifier,
666    codebase: &CodebaseMetadata,
667) -> Option<TAtomic> {
668    let signature = get_signature_of_function_like_identifier(function_like_identifier, codebase)?;
669
670    Some(TAtomic::Callable(TCallable::Signature(signature)))
671}
672
673#[must_use]
674pub fn get_signature_of_function_like_metadata(
675    function_like_identifier: &FunctionLikeIdentifier,
676    function_like_metadata: &FunctionLikeMetadata,
677    codebase: &CodebaseMetadata,
678    options: &TypeExpansionOptions,
679) -> TCallableSignature {
680    let parameters: Vec<_> = function_like_metadata
681        .parameters
682        .iter()
683        .map(|parameter_metadata| {
684            let type_signature = if let Some(t) = parameter_metadata.get_type_metadata() {
685                let mut t = t.type_union.clone();
686                expand_union(codebase, &mut t, options);
687                Some(Arc::new(t))
688            } else {
689                None
690            };
691
692            TCallableParameter::new(
693                type_signature,
694                parameter_metadata.flags.is_by_reference(),
695                parameter_metadata.flags.is_variadic(),
696                parameter_metadata.flags.has_default(),
697            )
698        })
699        .collect();
700
701    let return_type = if let Some(type_metadata) = function_like_metadata.return_type_metadata.as_ref() {
702        let mut return_type = type_metadata.type_union.clone();
703        expand_union(codebase, &mut return_type, options);
704        Some(Arc::new(return_type))
705    } else {
706        None
707    };
708
709    let mut signature = TCallableSignature::new(function_like_metadata.flags.is_pure(), true)
710        .with_parameters(parameters)
711        .with_return_type(return_type)
712        .with_source(Some(*function_like_identifier));
713
714    if let FunctionLikeIdentifier::Closure(file_id, closure_position) = function_like_identifier {
715        signature = signature.with_closure_location(Some((*file_id, *closure_position)));
716    }
717
718    signature
719}
720
721#[cold]
722fn expand_key_of(
723    return_type_key_of: &TKeyOf,
724    codebase: &CodebaseMetadata,
725    options: &TypeExpansionOptions,
726) -> Vec<TAtomic> {
727    let mut target_type = return_type_key_of.get_target_type().clone();
728    expand_union(codebase, &mut target_type, options);
729
730    let Some(new_return_types) = TKeyOf::get_key_of_targets(&target_type.types, codebase, false) else {
731        return vec![TAtomic::Derived(TDerived::KeyOf(return_type_key_of.clone()))];
732    };
733
734    new_return_types.types.into_owned()
735}
736
737#[cold]
738fn expand_value_of(
739    return_type_value_of: &TValueOf,
740    codebase: &CodebaseMetadata,
741    options: &TypeExpansionOptions,
742) -> Vec<TAtomic> {
743    let mut target_type = return_type_value_of.get_target_type().clone();
744    expand_union(codebase, &mut target_type, options);
745
746    let Some(new_return_types) = TValueOf::get_value_of_targets(&target_type.types, codebase, false) else {
747        return vec![TAtomic::Derived(TDerived::ValueOf(return_type_value_of.clone()))];
748    };
749
750    new_return_types.types.into_owned()
751}
752
753#[cold]
754fn expand_index_access(
755    return_type_index_access: &TIndexAccess,
756    codebase: &CodebaseMetadata,
757    options: &TypeExpansionOptions,
758) -> Vec<TAtomic> {
759    let mut target_type = return_type_index_access.get_target_type().clone();
760    expand_union(codebase, &mut target_type, options);
761
762    let mut index_type = return_type_index_access.get_index_type().clone();
763    expand_union(codebase, &mut index_type, options);
764
765    let Some(new_return_types) = TIndexAccess::get_indexed_access_result(&target_type.types, &index_type.types, false)
766    else {
767        return vec![TAtomic::Derived(TDerived::IndexAccess(return_type_index_access.clone()))];
768    };
769
770    new_return_types.types.into_owned()
771}
772
773#[cold]
774fn expand_int_mask(int_mask: &TIntMask, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
775    let mut literal_values = Vec::new();
776
777    for value in int_mask.get_values() {
778        let mut expanded = value.clone();
779        expand_union(codebase, &mut expanded, options);
780
781        if let Some(int_val) = expanded.get_single_literal_int_value() {
782            literal_values.push(int_val);
783        }
784    }
785
786    if literal_values.is_empty() {
787        return vec![TAtomic::Scalar(TScalar::int())];
788    }
789
790    let combinations = TIntMask::calculate_mask_combinations(&literal_values);
791    combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
792}
793
794#[cold]
795fn expand_int_mask_of(
796    int_mask_of: &TIntMaskOf,
797    codebase: &CodebaseMetadata,
798    options: &TypeExpansionOptions,
799) -> Vec<TAtomic> {
800    let mut target = int_mask_of.get_target_type().clone();
801    expand_union(codebase, &mut target, options);
802
803    let mut literal_values = Vec::new();
804    for atomic in target.types.iter() {
805        if let Some(int_val) = atomic.get_literal_int_value() {
806            literal_values.push(int_val);
807        }
808    }
809
810    if literal_values.is_empty() {
811        return vec![TAtomic::Scalar(TScalar::int())];
812    }
813
814    let combinations = TIntMask::calculate_mask_combinations(&literal_values);
815    combinations.into_iter().map(|v| TAtomic::Scalar(TScalar::literal_int(v))).collect()
816}
817
818#[cold]
819fn expand_properties_of(
820    properties_of: &TPropertiesOf,
821    codebase: &CodebaseMetadata,
822    options: &TypeExpansionOptions,
823) -> Vec<TAtomic> {
824    let mut target_type = properties_of.get_target_type().clone();
825    expand_union(codebase, &mut target_type, options);
826
827    let Some(keyed_array) =
828        TPropertiesOf::get_properties_of_targets(&target_type.types, codebase, properties_of.visibility(), false)
829    else {
830        return vec![TAtomic::Derived(TDerived::PropertiesOf(properties_of.clone()))];
831    };
832
833    vec![keyed_array]
834}
835
836#[cold]
837fn expand_alias(alias: &TAlias, codebase: &CodebaseMetadata, options: &TypeExpansionOptions) -> Vec<TAtomic> {
838    let class_name = alias.get_class_name();
839    let alias_name = alias.get_alias_name();
840
841    // Check for cycle using the HashSet
842    let is_cycle = EXPANDING_ALIASES.with(|set| set.borrow().contains(&(class_name, alias_name)));
843
844    if is_cycle {
845        return vec![TAtomic::Alias(alias.clone())];
846    }
847
848    let Some(mut expanded_union) = alias.resolve(codebase).cloned() else {
849        return vec![TAtomic::Alias(alias.clone())];
850    };
851
852    let _ = AliasExpansionGuard::new(class_name, alias_name);
853
854    expand_union(codebase, &mut expanded_union, options);
855
856    expanded_union.types.into_owned()
857}
858
859#[cfg(test)]
860mod tests {
861    use super::*;
862
863    use std::borrow::Cow;
864    use std::sync::Arc;
865
866    use bumpalo::Bump;
867
868    use mago_atom::atom;
869    use mago_database::Database;
870    use mago_database::DatabaseReader;
871    use mago_database::file::File;
872    use mago_database::file::FileId;
873    use mago_names::resolver::NameResolver;
874    use mago_span::Position;
875    use mago_syntax::parser::parse_file;
876
877    use crate::metadata::CodebaseMetadata;
878    use crate::misc::GenericParent;
879    use crate::populator::populate_codebase;
880    use crate::reference::SymbolReferences;
881    use crate::scanner::scan_program;
882    use crate::ttype::atomic::array::TArray;
883    use crate::ttype::atomic::array::keyed::TKeyedArray;
884    use crate::ttype::atomic::array::list::TList;
885    use crate::ttype::atomic::callable::TCallable;
886    use crate::ttype::atomic::callable::TCallableSignature;
887    use crate::ttype::atomic::callable::parameter::TCallableParameter;
888    use crate::ttype::atomic::conditional::TConditional;
889    use crate::ttype::atomic::derived::TDerived;
890    use crate::ttype::atomic::derived::index_access::TIndexAccess;
891    use crate::ttype::atomic::derived::key_of::TKeyOf;
892    use crate::ttype::atomic::derived::value_of::TValueOf;
893    use crate::ttype::atomic::generic::TGenericParameter;
894    use crate::ttype::atomic::iterable::TIterable;
895    use crate::ttype::atomic::object::r#enum::TEnum;
896    use crate::ttype::atomic::object::named::TNamedObject;
897    use crate::ttype::atomic::reference::TReference;
898    use crate::ttype::atomic::reference::TReferenceMemberSelector;
899    use crate::ttype::atomic::scalar::TScalar;
900    use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
901    use crate::ttype::atomic::scalar::class_like_string::TClassLikeStringKind;
902    use crate::ttype::flags::UnionFlags;
903    use crate::ttype::get_int;
904    use crate::ttype::get_mixed;
905    use crate::ttype::get_never;
906    use crate::ttype::get_null;
907    use crate::ttype::get_string;
908    use crate::ttype::get_void;
909
910    fn create_test_codebase(code: &'static str) -> CodebaseMetadata {
911        let file = File::ephemeral(Cow::Borrowed("code.php"), Cow::Borrowed(code));
912        let config =
913            mago_database::DatabaseConfiguration::new(std::path::Path::new("/"), vec![], vec![], vec![], vec![])
914                .into_static();
915        let database = Database::single(file, config);
916
917        let mut codebase = CodebaseMetadata::new();
918        let arena = Bump::new();
919        for file in database.files() {
920            let program = parse_file(&arena, &file);
921            assert!(!program.has_errors(), "Parse failed: {:?}", program.errors);
922            let resolved_names = NameResolver::new(&arena).resolve(program);
923            let program_codebase = scan_program(&arena, &file, program, &resolved_names);
924
925            codebase.extend(program_codebase);
926        }
927
928        populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
929
930        codebase
931    }
932
933    fn options_with_self(self_class: &str) -> TypeExpansionOptions {
934        TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
935    }
936
937    fn options_with_static(static_class: &str) -> TypeExpansionOptions {
938        TypeExpansionOptions {
939            self_class: Some(ascii_lowercase_atom(static_class)),
940            static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
941            ..Default::default()
942        }
943    }
944
945    fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
946        TypeExpansionOptions {
947            self_class: object.get_name(),
948            static_class_type: StaticClassType::Object(object),
949            ..Default::default()
950        }
951    }
952
953    macro_rules! assert_expands_to {
954        ($codebase:expr, $input:expr, $expected:expr) => {
955            assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
956        };
957        ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
958            let mut actual = $input.clone();
959            expand_union($codebase, &mut actual, $options);
960            assert_eq!(
961                actual.types.as_ref(),
962                $expected.types.as_ref(),
963                "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
964                $input,
965                $expected,
966                actual
967            );
968        }};
969    }
970
971    fn make_self_object() -> TUnion {
972        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
973    }
974
975    fn make_static_object() -> TUnion {
976        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
977    }
978
979    fn make_parent_object() -> TUnion {
980        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
981    }
982
983    fn make_named_object(name: &str) -> TUnion {
984        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
985    }
986
987    #[test]
988    fn test_expand_null_type() {
989        let codebase = CodebaseMetadata::new();
990        let null_type = get_null();
991        assert_expands_to!(&codebase, null_type, get_null());
992    }
993
994    #[test]
995    fn test_expand_void_type() {
996        let codebase = CodebaseMetadata::new();
997        let void_type = get_void();
998        assert_expands_to!(&codebase, void_type, get_void());
999    }
1000
1001    #[test]
1002    fn test_expand_never_type() {
1003        let codebase = CodebaseMetadata::new();
1004        let never_type = get_never();
1005        assert_expands_to!(&codebase, never_type, get_never());
1006    }
1007
1008    #[test]
1009    fn test_expand_int_type() {
1010        let codebase = CodebaseMetadata::new();
1011        let int_type = get_int();
1012        assert_expands_to!(&codebase, int_type, get_int());
1013    }
1014
1015    #[test]
1016    fn test_expand_mixed_type() {
1017        let codebase = CodebaseMetadata::new();
1018        let mixed_type = get_mixed();
1019        assert_expands_to!(&codebase, mixed_type, get_mixed());
1020    }
1021
1022    #[test]
1023    fn test_expand_keyed_array_with_self_key() {
1024        let code = r"<?php class Foo {}";
1025        let codebase = create_test_codebase(code);
1026
1027        let mut keyed = TKeyedArray::new();
1028        keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
1029        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1030
1031        let options = options_with_self("Foo");
1032        let mut actual = input.clone();
1033        expand_union(&codebase, &mut actual, &options);
1034
1035        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1036            && let Some((key, _)) = &keyed.parameters
1037        {
1038            assert!(key.types.iter().any(|t| {
1039                if let TAtomic::Object(TObject::Named(named)) = t {
1040                    named.name == ascii_lowercase_atom("foo")
1041                } else {
1042                    false
1043                }
1044            }));
1045        }
1046    }
1047
1048    #[test]
1049    fn test_expand_keyed_array_with_self_value() {
1050        let code = r"<?php class Foo {}";
1051        let codebase = create_test_codebase(code);
1052
1053        let mut keyed = TKeyedArray::new();
1054        keyed.parameters = Some((Arc::new(get_string()), Arc::new(make_self_object())));
1055        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1056
1057        let options = options_with_self("Foo");
1058        let mut actual = input.clone();
1059        expand_union(&codebase, &mut actual, &options);
1060
1061        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1062            && let Some((_, value)) = &keyed.parameters
1063        {
1064            assert!(value.types.iter().any(|t| {
1065                if let TAtomic::Object(TObject::Named(named)) = t {
1066                    named.name == ascii_lowercase_atom("foo")
1067                } else {
1068                    false
1069                }
1070            }));
1071        }
1072    }
1073
1074    #[test]
1075    fn test_expand_keyed_array_known_items() {
1076        let code = r"<?php class Foo {}";
1077        let codebase = create_test_codebase(code);
1078
1079        use crate::ttype::atomic::array::key::ArrayKey;
1080        use std::collections::BTreeMap;
1081
1082        let mut keyed = TKeyedArray::new();
1083        let mut known_items = BTreeMap::new();
1084        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
1085        keyed.known_items = Some(known_items);
1086        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1087
1088        let options = options_with_self("Foo");
1089        let mut actual = input.clone();
1090        expand_union(&codebase, &mut actual, &options);
1091
1092        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1093            && let Some(items) = &keyed.known_items
1094        {
1095            let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
1096            assert!(item_type.types.iter().any(|t| {
1097                if let TAtomic::Object(TObject::Named(named)) = t {
1098                    named.name == ascii_lowercase_atom("foo")
1099                } else {
1100                    false
1101                }
1102            }));
1103        }
1104    }
1105
1106    #[test]
1107    fn test_expand_list_with_self_element() {
1108        let code = r"<?php class Foo {}";
1109        let codebase = create_test_codebase(code);
1110
1111        let list = TList::new(Arc::new(make_self_object()));
1112        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1113
1114        let options = options_with_self("Foo");
1115        let mut actual = input.clone();
1116        expand_union(&codebase, &mut actual, &options);
1117
1118        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1119            assert!(list.element_type.types.iter().any(|t| {
1120                if let TAtomic::Object(TObject::Named(named)) = t {
1121                    named.name == ascii_lowercase_atom("foo")
1122                } else {
1123                    false
1124                }
1125            }));
1126        }
1127    }
1128
1129    #[test]
1130    fn test_expand_list_known_elements() {
1131        let code = r"<?php class Foo {}";
1132        let codebase = create_test_codebase(code);
1133
1134        use std::collections::BTreeMap;
1135
1136        let mut list = TList::new(Arc::new(get_mixed()));
1137        let mut known_elements = BTreeMap::new();
1138        known_elements.insert(0, (false, make_self_object()));
1139        list.known_elements = Some(known_elements);
1140        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1141
1142        let options = options_with_self("Foo");
1143        let mut actual = input.clone();
1144        expand_union(&codebase, &mut actual, &options);
1145
1146        if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1147            && let Some(elements) = &list.known_elements
1148        {
1149            let (_, element_type) = elements.get(&0).unwrap();
1150            assert!(element_type.types.iter().any(|t| {
1151                if let TAtomic::Object(TObject::Named(named)) = t {
1152                    named.name == ascii_lowercase_atom("foo")
1153                } else {
1154                    false
1155                }
1156            }));
1157        }
1158    }
1159
1160    #[test]
1161    fn test_expand_nested_array() {
1162        let code = r"<?php class Foo {}";
1163        let codebase = create_test_codebase(code);
1164
1165        let inner_list = TList::new(Arc::new(make_self_object()));
1166        let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1167
1168        let mut outer = TKeyedArray::new();
1169        outer.parameters = Some((Arc::new(make_self_object()), Arc::new(inner_array)));
1170        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1171
1172        let options = options_with_self("Foo");
1173        let mut actual = input.clone();
1174        expand_union(&codebase, &mut actual, &options);
1175
1176        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1177            && let Some((key, value)) = &keyed.parameters
1178        {
1179            assert!(key.types.iter().any(|t| {
1180                if let TAtomic::Object(TObject::Named(named)) = t {
1181                    named.name == ascii_lowercase_atom("foo")
1182                } else {
1183                    false
1184                }
1185            }));
1186            if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1187                assert!(inner.element_type.types.iter().any(|t| {
1188                    if let TAtomic::Object(TObject::Named(named)) = t {
1189                        named.name == ascii_lowercase_atom("foo")
1190                    } else {
1191                        false
1192                    }
1193                }));
1194            }
1195        }
1196    }
1197
1198    #[test]
1199    fn test_expand_empty_array() {
1200        let codebase = CodebaseMetadata::new();
1201        let keyed = TKeyedArray::new();
1202        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1203        let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1204        assert_expands_to!(&codebase, input, expected);
1205    }
1206
1207    #[test]
1208    fn test_expand_non_empty_list() {
1209        let code = r"<?php class Foo {}";
1210        let codebase = create_test_codebase(code);
1211
1212        let mut list = TList::new(Arc::new(make_self_object()));
1213        list.non_empty = true;
1214        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1215
1216        let options = options_with_self("Foo");
1217        let mut actual = input.clone();
1218        expand_union(&codebase, &mut actual, &options);
1219
1220        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1221            assert!(list.non_empty);
1222            assert!(list.element_type.types.iter().any(|t| {
1223                if let TAtomic::Object(TObject::Named(named)) = t {
1224                    named.name == ascii_lowercase_atom("foo")
1225                } else {
1226                    false
1227                }
1228            }));
1229        }
1230    }
1231
1232    #[test]
1233    fn test_expand_self_to_class_name() {
1234        let code = r"<?php class Foo {}";
1235        let codebase = create_test_codebase(code);
1236
1237        let input = make_self_object();
1238        let options = options_with_self("Foo");
1239        let mut actual = input.clone();
1240        expand_union(&codebase, &mut actual, &options);
1241
1242        assert!(actual.types.iter().any(|t| {
1243            if let TAtomic::Object(TObject::Named(named)) = t {
1244                named.name == ascii_lowercase_atom("foo")
1245            } else {
1246                false
1247            }
1248        }));
1249    }
1250
1251    #[test]
1252    fn test_expand_static_to_class_name() {
1253        let code = r"<?php class Foo {}";
1254        let codebase = create_test_codebase(code);
1255
1256        let input = make_static_object();
1257        let options = options_with_static("Foo");
1258        let mut actual = input.clone();
1259        expand_union(&codebase, &mut actual, &options);
1260
1261        assert!(actual.types.iter().any(|t| {
1262            if let TAtomic::Object(TObject::Named(named)) = t {
1263                named.name == ascii_lowercase_atom("foo")
1264            } else {
1265                false
1266            }
1267        }));
1268    }
1269
1270    #[test]
1271    fn test_expand_static_with_object_type() {
1272        let code = r"<?php class Foo {}";
1273        let codebase = create_test_codebase(code);
1274
1275        let input = make_static_object();
1276        let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1277        let options = options_with_static_object(static_obj);
1278        let mut actual = input.clone();
1279        expand_union(&codebase, &mut actual, &options);
1280
1281        assert!(actual.types.iter().any(|t| {
1282            if let TAtomic::Object(TObject::Named(named)) = t {
1283                named.name == ascii_lowercase_atom("foo") && named.is_static && !named.is_this
1284            } else {
1285                false
1286            }
1287        }));
1288    }
1289
1290    #[test]
1291    fn test_expand_static_with_enum_type() {
1292        let code = r"<?php enum Status { case Active; case Inactive; }";
1293        let codebase = create_test_codebase(code);
1294
1295        let input = make_static_object();
1296        let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1297        let options = options_with_static_object(static_enum);
1298        let mut actual = input.clone();
1299        expand_union(&codebase, &mut actual, &options);
1300
1301        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1302    }
1303
1304    #[test]
1305    fn test_expand_parent_to_parent_class() {
1306        let code = r"<?php
1307            class BaseClass {}
1308            class ChildClass extends BaseClass {}
1309        ";
1310        let codebase = create_test_codebase(code);
1311
1312        let input = make_parent_object();
1313        let options = options_with_self("ChildClass");
1314        let mut actual = input.clone();
1315        expand_union(&codebase, &mut actual, &options);
1316
1317        assert!(actual.types.iter().any(|t| {
1318            if let TAtomic::Object(TObject::Named(named)) = t {
1319                named.name == ascii_lowercase_atom("baseclass")
1320            } else {
1321                false
1322            }
1323        }));
1324    }
1325
1326    #[test]
1327    fn test_expand_parent_without_parent_class() {
1328        let code = r"<?php class Foo {}";
1329        let codebase = create_test_codebase(code);
1330
1331        let input = make_parent_object();
1332        let options = options_with_self("Foo");
1333        let mut actual = input.clone();
1334        expand_union(&codebase, &mut actual, &options);
1335
1336        assert!(actual.types.iter().any(|t| {
1337            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1338        }));
1339    }
1340
1341    #[test]
1342    fn test_expand_this_variable() {
1343        let code = r"<?php class Foo {}";
1344        let codebase = create_test_codebase(code);
1345
1346        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1347        let options = options_with_static("Foo");
1348        let mut actual = input.clone();
1349        expand_union(&codebase, &mut actual, &options);
1350
1351        assert!(actual.types.iter().any(|t| {
1352            if let TAtomic::Object(TObject::Named(named)) = t {
1353                named.name == ascii_lowercase_atom("foo")
1354            } else {
1355                false
1356            }
1357        }));
1358    }
1359
1360    #[test]
1361    fn test_expand_this_with_final_function() {
1362        let code = r"<?php class Foo {}";
1363        let codebase = create_test_codebase(code);
1364
1365        let input = make_static_object();
1366        let options = TypeExpansionOptions {
1367            self_class: Some(ascii_lowercase_atom("foo")),
1368            static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1369            function_is_final: true,
1370            ..Default::default()
1371        };
1372        let mut actual = input.clone();
1373        expand_union(&codebase, &mut actual, &options);
1374
1375        assert!(actual.types.iter().any(|t| {
1376            if let TAtomic::Object(TObject::Named(named)) = t {
1377                named.name == ascii_lowercase_atom("foo") && !named.is_this
1378            } else {
1379                false
1380            }
1381        }));
1382    }
1383
1384    #[test]
1385    fn test_expand_object_with_type_parameters() {
1386        let code = r"<?php class Container {}";
1387        let codebase = create_test_codebase(code);
1388
1389        let named =
1390            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1391        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1392
1393        let options = options_with_self("Foo");
1394        let mut actual = input.clone();
1395        expand_union(&codebase, &mut actual, &options);
1396
1397        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1398            && let Some(params) = &named.type_parameters
1399        {
1400            assert!(params[0].types.iter().any(|t| {
1401                if let TAtomic::Object(TObject::Named(named)) = t {
1402                    named.name == ascii_lowercase_atom("foo")
1403                } else {
1404                    false
1405                }
1406            }));
1407        }
1408    }
1409
1410    #[test]
1411    fn test_expand_object_gets_default_type_params() {
1412        let code = r"<?php
1413            /** @template T */
1414            class Container {}
1415        ";
1416        let codebase = create_test_codebase(code);
1417
1418        let named = TNamedObject::new(ascii_lowercase_atom("container"));
1419        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1420
1421        let mut actual = input.clone();
1422        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1423
1424        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1425            assert!(named.type_parameters.is_some());
1426        }
1427    }
1428
1429    #[test]
1430    fn test_expand_object_intersection_from_static() {
1431        let code = r"<?php
1432            interface Stringable {}
1433            class Foo implements Stringable {}
1434        ";
1435        let codebase = create_test_codebase(code);
1436
1437        let input = make_static_object();
1438
1439        let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1440        static_named.intersection_types =
1441            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1442        let static_obj = TObject::Named(static_named);
1443        let options = options_with_static_object(static_obj);
1444
1445        let mut actual = input.clone();
1446        expand_union(&codebase, &mut actual, &options);
1447
1448        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1449            assert!(named.intersection_types.is_some());
1450        }
1451    }
1452
1453    #[test]
1454    fn test_expand_self_without_self_class_option() {
1455        let codebase = CodebaseMetadata::new();
1456
1457        let input = make_self_object();
1458        let mut actual = input.clone();
1459        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1460
1461        assert!(actual.types.iter().any(|t| {
1462            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1463        }));
1464    }
1465
1466    #[test]
1467    fn test_expand_callable_return_type() {
1468        let code = r"<?php class Foo {}";
1469        let codebase = create_test_codebase(code);
1470
1471        let sig = TCallableSignature::new(false, false).with_return_type(Some(Arc::new(make_self_object())));
1472        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1473
1474        let options = options_with_self("Foo");
1475        let mut actual = input.clone();
1476        expand_union(&codebase, &mut actual, &options);
1477
1478        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1479            && let Some(ret) = sig.get_return_type()
1480        {
1481            assert!(ret.types.iter().any(|t| {
1482                if let TAtomic::Object(TObject::Named(named)) = t {
1483                    named.name == ascii_lowercase_atom("foo")
1484                } else {
1485                    false
1486                }
1487            }));
1488        }
1489    }
1490
1491    #[test]
1492    fn test_expand_callable_parameter_types() {
1493        let code = r"<?php class Foo {}";
1494        let codebase = create_test_codebase(code);
1495
1496        let param = TCallableParameter::new(Some(Arc::new(make_self_object())), false, false, false);
1497        let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1498        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1499
1500        let options = options_with_self("Foo");
1501        let mut actual = input.clone();
1502        expand_union(&codebase, &mut actual, &options);
1503
1504        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1505            && let Some(param) = sig.get_parameters().first()
1506            && let Some(param_type) = param.get_type_signature()
1507        {
1508            assert!(param_type.types.iter().any(|t| {
1509                if let TAtomic::Object(TObject::Named(named)) = t {
1510                    named.name == ascii_lowercase_atom("foo")
1511                } else {
1512                    false
1513                }
1514            }));
1515        }
1516    }
1517
1518    #[test]
1519    fn test_expand_callable_alias_to_function() {
1520        let code = r"<?php
1521            function myFunc(): int { return 1; }
1522        ";
1523        let codebase = create_test_codebase(code);
1524
1525        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1526        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1527
1528        let mut actual = input.clone();
1529        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1530
1531        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1532    }
1533
1534    #[test]
1535    fn test_expand_callable_alias_to_method() {
1536        let code = r"<?php
1537            class Foo {
1538                public function bar(): int { return 1; }
1539            }
1540        ";
1541        let codebase = create_test_codebase(code);
1542
1543        let alias =
1544            TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1545        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1546
1547        let mut actual = input.clone();
1548        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1549
1550        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1551    }
1552
1553    #[test]
1554    fn test_expand_callable_alias_unknown() {
1555        let codebase = CodebaseMetadata::new();
1556
1557        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1558        let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1559
1560        let mut actual = input.clone();
1561        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1562
1563        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1564    }
1565
1566    #[test]
1567    fn test_expand_closure_signature() {
1568        let code = r"<?php class Foo {}";
1569        let codebase = create_test_codebase(code);
1570
1571        let sig = TCallableSignature::new(false, true).with_return_type(Some(Arc::new(make_self_object())));
1572        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1573
1574        let options = options_with_self("Foo");
1575        let mut actual = input.clone();
1576        expand_union(&codebase, &mut actual, &options);
1577
1578        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1579            && let Some(ret) = sig.get_return_type()
1580        {
1581            assert!(ret.types.iter().any(|t| {
1582                if let TAtomic::Object(TObject::Named(named)) = t {
1583                    named.name == ascii_lowercase_atom("foo")
1584                } else {
1585                    false
1586                }
1587            }));
1588        }
1589    }
1590
1591    #[test]
1592    fn test_expand_generic_parameter_constraint() {
1593        let code = r"<?php class Foo {}";
1594        let codebase = create_test_codebase(code);
1595
1596        let generic = TGenericParameter::new(
1597            atom("T"),
1598            Arc::new(make_self_object()),
1599            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1600        );
1601        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1602
1603        let options = options_with_self("Foo");
1604        let mut actual = input.clone();
1605        expand_union(&codebase, &mut actual, &options);
1606
1607        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1608            assert!(param.constraint.types.iter().any(|t| {
1609                if let TAtomic::Object(TObject::Named(named)) = t {
1610                    named.name == ascii_lowercase_atom("foo")
1611                } else {
1612                    false
1613                }
1614            }));
1615        }
1616    }
1617
1618    #[test]
1619    fn test_expand_nested_generic_constraint() {
1620        let code = r"<?php class Foo {} class Bar {}";
1621        let codebase = create_test_codebase(code);
1622
1623        let container =
1624            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1625        let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1626
1627        let generic = TGenericParameter::new(
1628            atom("T"),
1629            Arc::new(constraint),
1630            GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1631        );
1632        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1633
1634        let options = options_with_self("Foo");
1635        let mut actual = input.clone();
1636        expand_union(&codebase, &mut actual, &options);
1637
1638        if let TAtomic::GenericParameter(param) = &actual.types[0]
1639            && let TAtomic::Object(TObject::Named(named)) = &param.constraint.types[0]
1640            && let Some(params) = &named.type_parameters
1641        {
1642            assert!(params[0].types.iter().any(|t| {
1643                if let TAtomic::Object(TObject::Named(named)) = t {
1644                    named.name == ascii_lowercase_atom("foo")
1645                } else {
1646                    false
1647                }
1648            }));
1649        }
1650    }
1651
1652    #[test]
1653    fn test_expand_generic_with_intersection() {
1654        let code = r"<?php
1655            interface Stringable {}
1656            class Foo {}
1657        ";
1658        let codebase = create_test_codebase(code);
1659
1660        let mut generic = TGenericParameter::new(
1661            atom("T"),
1662            Arc::new(make_self_object()),
1663            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1664        );
1665        generic.intersection_types =
1666            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1667        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1668
1669        let options = options_with_self("Foo");
1670        let mut actual = input.clone();
1671        expand_union(&codebase, &mut actual, &options);
1672
1673        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1674            assert!(param.intersection_types.is_some());
1675            assert!(param.constraint.types.iter().any(|t| {
1676                if let TAtomic::Object(TObject::Named(named)) = t {
1677                    named.name == ascii_lowercase_atom("foo")
1678                } else {
1679                    false
1680                }
1681            }));
1682        }
1683    }
1684
1685    #[test]
1686    fn test_expand_class_string_of_self() {
1687        let code = r"<?php class Foo {}";
1688        let codebase = create_test_codebase(code);
1689
1690        let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1691        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1692        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1693
1694        let options = options_with_self("Foo");
1695        let mut actual = input.clone();
1696        expand_union(&codebase, &mut actual, &options);
1697
1698        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1699            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1700        {
1701            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1702        }
1703    }
1704
1705    #[test]
1706    fn test_expand_class_string_of_static() {
1707        let code = r"<?php class Foo {}";
1708        let codebase = create_test_codebase(code);
1709
1710        let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1711        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1712        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1713
1714        let options = options_with_static("Foo");
1715        let mut actual = input.clone();
1716        expand_union(&codebase, &mut actual, &options);
1717
1718        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1719            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1720        {
1721            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1722        }
1723    }
1724
1725    #[test]
1726    fn test_expand_interface_string_of_type() {
1727        let code = r"<?php interface MyInterface {}";
1728        let codebase = create_test_codebase(code);
1729
1730        let constraint = Arc::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1731        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1732        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1733
1734        let options = options_with_self("MyInterface");
1735        let mut actual = input.clone();
1736        expand_union(&codebase, &mut actual, &options);
1737
1738        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1739            &actual.types[0]
1740        {
1741            assert!(matches!(kind, TClassLikeStringKind::Interface));
1742            if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1743                assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1744            }
1745        }
1746    }
1747
1748    #[test]
1749    fn test_expand_member_reference_wildcard_constants() {
1750        let code = r"<?php
1751            class Foo {
1752                public const A = 1;
1753                public const B = 2;
1754            }
1755        ";
1756        let codebase = create_test_codebase(code);
1757
1758        let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1759        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1760
1761        let mut actual = input.clone();
1762        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1763
1764        assert!(!actual.types.is_empty());
1765    }
1766
1767    #[test]
1768    fn test_expand_member_reference_wildcard_enum_cases() {
1769        let code = r"<?php
1770            enum Status {
1771                case Active;
1772                case Inactive;
1773            }
1774        ";
1775        let codebase = create_test_codebase(code);
1776
1777        let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1778        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1779
1780        let mut actual = input.clone();
1781        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1782
1783        assert_eq!(actual.types.len(), 2);
1784        assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1785    }
1786
1787    #[test]
1788    fn test_expand_member_reference_starts_with() {
1789        let code = r"<?php
1790            class Foo {
1791                public const STATUS_ACTIVE = 1;
1792                public const STATUS_INACTIVE = 2;
1793                public const OTHER = 3;
1794            }
1795        ";
1796        let codebase = create_test_codebase(code);
1797
1798        let reference =
1799            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
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_ends_with() {
1810        let code = r"<?php
1811            class Foo {
1812                public const READ_ERROR = 1;
1813                public const WRITE_ERROR = 2;
1814                public const SUCCESS = 0;
1815            }
1816        ";
1817        let codebase = create_test_codebase(code);
1818
1819        let reference =
1820            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
1821        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1822
1823        let mut actual = input.clone();
1824        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1825
1826        assert!(!actual.types.is_empty());
1827    }
1828
1829    #[test]
1830    fn test_expand_member_reference_identifier_constant() {
1831        let code = r"<?php
1832            class Foo {
1833                public const BAR = 42;
1834            }
1835        ";
1836        let codebase = create_test_codebase(code);
1837
1838        let reference =
1839            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
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    }
1847
1848    #[test]
1849    fn test_expand_member_reference_identifier_enum_case() {
1850        let code = r"<?php
1851            enum Status {
1852                case Active;
1853            }
1854        ";
1855        let codebase = create_test_codebase(code);
1856
1857        let reference = TReference::new_member(
1858            ascii_lowercase_atom("status"),
1859            TReferenceMemberSelector::Identifier(atom("Active")),
1860        );
1861        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1862
1863        let mut actual = input.clone();
1864        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1865
1866        assert_eq!(actual.types.len(), 1);
1867        assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1868    }
1869
1870    #[test]
1871    fn test_expand_member_reference_unknown_class() {
1872        let codebase = CodebaseMetadata::new();
1873
1874        let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1875        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1876
1877        let mut actual = input.clone();
1878        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1879
1880        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1881    }
1882
1883    #[test]
1884    fn test_expand_member_reference_unknown_member() {
1885        let code = r"<?php class Foo {}";
1886        let codebase = create_test_codebase(code);
1887
1888        let reference = TReference::new_member(
1889            ascii_lowercase_atom("foo"),
1890            TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1891        );
1892        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1893
1894        let mut actual = input.clone();
1895        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1896
1897        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1898    }
1899
1900    #[test]
1901    fn test_expand_member_reference_constant_with_inferred_type() {
1902        let code = r#"<?php
1903            class Foo {
1904                public const VALUE = "hello";
1905            }
1906        "#;
1907        let codebase = create_test_codebase(code);
1908
1909        let reference =
1910            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1911        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1912
1913        let mut actual = input.clone();
1914        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1915
1916        assert_eq!(actual.types.len(), 1);
1917    }
1918
1919    #[test]
1920    fn test_expand_member_reference_constant_with_type_metadata() {
1921        let code = r"<?php
1922            class Foo {
1923                /** @var int */
1924                public const VALUE = 42;
1925            }
1926        ";
1927        let codebase = create_test_codebase(code);
1928
1929        let reference =
1930            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1931        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1932
1933        let mut actual = input.clone();
1934        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1935
1936        assert_eq!(actual.types.len(), 1);
1937    }
1938
1939    #[test]
1940    fn test_expand_conditional_both_branches() {
1941        let code = r"<?php class Foo {} class Bar {}";
1942        let codebase = create_test_codebase(code);
1943
1944        let conditional = TConditional::new(
1945            Arc::new(get_mixed()),
1946            Arc::new(get_string()),
1947            Arc::new(make_self_object()),
1948            Arc::new(make_self_object()),
1949            false,
1950        );
1951        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1952
1953        let options = options_with_self("Foo");
1954        let mut actual = input.clone();
1955        expand_union(&codebase, &mut actual, &options);
1956
1957        assert!(actual.types.iter().any(|t| {
1958            if let TAtomic::Object(TObject::Named(named)) = t {
1959                named.name == ascii_lowercase_atom("foo")
1960            } else {
1961                false
1962            }
1963        }));
1964    }
1965
1966    #[test]
1967    fn test_expand_conditional_with_self_in_then() {
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(make_self_object()),
1975            Arc::new(get_int()),
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_conditional_with_self_in_otherwise() {
1989        let code = r"<?php class Foo {}";
1990        let codebase = create_test_codebase(code);
1991
1992        let conditional = TConditional::new(
1993            Arc::new(get_mixed()),
1994            Arc::new(get_string()),
1995            Arc::new(get_int()),
1996            Arc::new(make_self_object()),
1997            false,
1998        );
1999        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
2000
2001        let options = options_with_self("Foo");
2002        let mut actual = input.clone();
2003        expand_union(&codebase, &mut actual, &options);
2004
2005        assert!(!actual.types.is_empty());
2006    }
2007
2008    #[test]
2009    fn test_expand_simple_alias() {
2010        let code = r"<?php
2011            class Foo {
2012                /** @phpstan-type MyInt = int */
2013            }
2014        ";
2015        let codebase = create_test_codebase(code);
2016
2017        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
2018        let input = TUnion::from_atomic(TAtomic::Alias(alias));
2019
2020        let mut actual = input.clone();
2021        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2022
2023        assert!(!actual.types.is_empty());
2024    }
2025
2026    #[test]
2027    fn test_expand_nested_alias() {
2028        let code = r"<?php
2029            class Foo {
2030                /** @phpstan-type Inner = int */
2031                /** @phpstan-type Outer = Inner */
2032            }
2033        ";
2034        let codebase = create_test_codebase(code);
2035
2036        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
2037        let input = TUnion::from_atomic(TAtomic::Alias(alias));
2038
2039        let mut actual = input.clone();
2040        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2041
2042        assert!(!actual.types.is_empty());
2043    }
2044
2045    #[test]
2046    fn test_expand_alias_cycle_detection() {
2047        let codebase = CodebaseMetadata::new();
2048
2049        let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
2050        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
2051
2052        let mut actual = input.clone();
2053        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2054
2055        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2056    }
2057
2058    #[test]
2059    fn test_expand_alias_unknown() {
2060        let codebase = CodebaseMetadata::new();
2061
2062        let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
2063        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
2064
2065        let mut actual = input.clone();
2066        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2067
2068        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
2069    }
2070
2071    #[test]
2072    fn test_expand_alias_with_self_inside() {
2073        let code = r"<?php
2074            class Foo {
2075                /** @phpstan-type MySelf = self */
2076            }
2077        ";
2078        let codebase = create_test_codebase(code);
2079
2080        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
2081        let input = TUnion::from_atomic(TAtomic::Alias(alias));
2082
2083        let options = options_with_self("Foo");
2084        let mut actual = input.clone();
2085        expand_union(&codebase, &mut actual, &options);
2086
2087        assert!(!actual.types.is_empty());
2088    }
2089
2090    #[test]
2091    fn test_expand_key_of_array() {
2092        let codebase = CodebaseMetadata::new();
2093
2094        let mut keyed = TKeyedArray::new();
2095        keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2096        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2097
2098        let key_of = TKeyOf::new(Arc::new(array_type));
2099        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2100
2101        let mut actual = input.clone();
2102        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2103
2104        assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
2105    }
2106
2107    #[test]
2108    fn test_expand_key_of_with_self() {
2109        let code = r"<?php class Foo {}";
2110        let codebase = create_test_codebase(code);
2111
2112        let mut keyed = TKeyedArray::new();
2113        keyed.parameters = Some((Arc::new(make_self_object()), Arc::new(get_int())));
2114        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2115
2116        let key_of = TKeyOf::new(Arc::new(array_type));
2117        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2118
2119        let options = options_with_self("Foo");
2120        let mut actual = input.clone();
2121        expand_union(&codebase, &mut actual, &options);
2122
2123        assert!(!actual.types.is_empty());
2124    }
2125
2126    #[test]
2127    fn test_expand_value_of_array() {
2128        let codebase = CodebaseMetadata::new();
2129
2130        let mut keyed = TKeyedArray::new();
2131        keyed.parameters = Some((Arc::new(get_string()), Arc::new(get_int())));
2132        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2133
2134        let value_of = TValueOf::new(Arc::new(array_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.iter().any(super::super::atomic::TAtomic::is_int));
2141    }
2142
2143    #[test]
2144    fn test_expand_value_of_enum() {
2145        let code = r"<?php
2146            enum Status: string {
2147                case Active = 'active';
2148                case Inactive = 'inactive';
2149            }
2150        ";
2151        let codebase = create_test_codebase(code);
2152
2153        let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
2154
2155        let value_of = TValueOf::new(Arc::new(enum_type));
2156        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2157
2158        let mut actual = input.clone();
2159        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2160
2161        assert!(!actual.types.is_empty());
2162    }
2163
2164    #[test]
2165    fn test_expand_index_access() {
2166        let codebase = CodebaseMetadata::new();
2167
2168        use crate::ttype::atomic::array::key::ArrayKey;
2169        use std::collections::BTreeMap;
2170
2171        let mut keyed = TKeyedArray::new();
2172        let mut known_items = BTreeMap::new();
2173        known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2174        keyed.known_items = Some(known_items);
2175        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2176
2177        use crate::ttype::get_literal_string;
2178        let index_type = get_literal_string(atom("key"));
2179
2180        let index_access = TIndexAccess::new(array_type, index_type);
2181        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2182
2183        let mut actual = input.clone();
2184        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2185
2186        assert!(!actual.types.is_empty());
2187    }
2188
2189    #[test]
2190    fn test_expand_index_access_with_self() {
2191        let code = r"<?php class Foo {}";
2192        let codebase = create_test_codebase(code);
2193
2194        use crate::ttype::atomic::array::key::ArrayKey;
2195        use std::collections::BTreeMap;
2196
2197        let mut keyed = TKeyedArray::new();
2198        let mut known_items = BTreeMap::new();
2199        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2200        keyed.known_items = Some(known_items);
2201        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2202
2203        use crate::ttype::get_literal_string;
2204        let index_type = get_literal_string(atom("key"));
2205
2206        let index_access = TIndexAccess::new(array_type, index_type);
2207        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2208
2209        let options = options_with_self("Foo");
2210        let mut actual = input.clone();
2211        expand_union(&codebase, &mut actual, &options);
2212
2213        assert!(!actual.types.is_empty());
2214    }
2215
2216    #[test]
2217    fn test_expand_iterable_key_type() {
2218        let code = r"<?php class Foo {}";
2219        let codebase = create_test_codebase(code);
2220
2221        let iterable = TIterable::new(Arc::new(make_self_object()), Arc::new(get_int()));
2222        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2223
2224        let options = options_with_self("Foo");
2225        let mut actual = input.clone();
2226        expand_union(&codebase, &mut actual, &options);
2227
2228        if let TAtomic::Iterable(iter) = &actual.types[0] {
2229            assert!(iter.get_key_type().types.iter().any(|t| {
2230                if let TAtomic::Object(TObject::Named(named)) = t {
2231                    named.name == ascii_lowercase_atom("foo")
2232                } else {
2233                    false
2234                }
2235            }));
2236        }
2237    }
2238
2239    #[test]
2240    fn test_expand_iterable_value_type() {
2241        let code = r"<?php class Foo {}";
2242        let codebase = create_test_codebase(code);
2243
2244        let iterable = TIterable::new(Arc::new(get_int()), Arc::new(make_self_object()));
2245        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2246
2247        let options = options_with_self("Foo");
2248        let mut actual = input.clone();
2249        expand_union(&codebase, &mut actual, &options);
2250
2251        if let TAtomic::Iterable(iter) = &actual.types[0] {
2252            assert!(iter.get_value_type().types.iter().any(|t| {
2253                if let TAtomic::Object(TObject::Named(named)) = t {
2254                    named.name == ascii_lowercase_atom("foo")
2255                } else {
2256                    false
2257                }
2258            }));
2259        }
2260    }
2261
2262    #[test]
2263    fn test_get_signature_of_function() {
2264        let code = r#"<?php
2265            function myFunc(int $a): string { return ""; }
2266        "#;
2267        let codebase = create_test_codebase(code);
2268
2269        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2270
2271        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2272        assert!(sig.is_some());
2273
2274        let sig = sig.unwrap();
2275        assert_eq!(sig.get_parameters().len(), 1);
2276        assert!(sig.get_return_type().is_some());
2277    }
2278
2279    #[test]
2280    fn test_get_signature_of_method() {
2281        let code = r"<?php
2282            class Foo {
2283                public function bar(string $s): int { return 0; }
2284            }
2285        ";
2286        let codebase = create_test_codebase(code);
2287
2288        let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2289
2290        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2291        assert!(sig.is_some());
2292
2293        let sig = sig.unwrap();
2294        assert_eq!(sig.get_parameters().len(), 1);
2295    }
2296
2297    #[test]
2298    fn test_get_signature_of_closure() {
2299        let codebase = CodebaseMetadata::new();
2300
2301        let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2302        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2303
2304        assert!(sig.is_none());
2305    }
2306
2307    #[test]
2308    fn test_get_atomic_of_function() {
2309        let code = r"<?php
2310            function myFunc(): void {}
2311        ";
2312        let codebase = create_test_codebase(code);
2313
2314        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2315
2316        let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2317        assert!(atomic.is_some());
2318        assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2319    }
2320
2321    #[test]
2322    fn test_get_signature_with_parameters() {
2323        let code = r"<?php
2324            function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2325        ";
2326        let codebase = create_test_codebase(code);
2327
2328        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2329
2330        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2331        assert!(sig.is_some());
2332
2333        let sig = sig.unwrap();
2334        assert_eq!(sig.get_parameters().len(), 3);
2335
2336        let third_param = &sig.get_parameters()[2];
2337        assert!(third_param.has_default());
2338    }
2339
2340    #[test]
2341    fn test_expand_preserves_by_reference_flag() {
2342        let code = r"<?php class Foo {}";
2343        let codebase = create_test_codebase(code);
2344
2345        let mut input = make_self_object();
2346        input.flags.insert(UnionFlags::BY_REFERENCE);
2347
2348        let options = options_with_self("Foo");
2349        let mut actual = input.clone();
2350        expand_union(&codebase, &mut actual, &options);
2351
2352        assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2353    }
2354
2355    #[test]
2356    fn test_expand_preserves_possibly_undefined_flag() {
2357        let code = r"<?php class Foo {}";
2358        let codebase = create_test_codebase(code);
2359
2360        let mut input = make_self_object();
2361        input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2362
2363        let options = options_with_self("Foo");
2364        let mut actual = input.clone();
2365        expand_union(&codebase, &mut actual, &options);
2366
2367        assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2368    }
2369
2370    #[test]
2371    fn test_expand_multiple_self_in_union() {
2372        let code = r"<?php class Foo {}";
2373        let codebase = create_test_codebase(code);
2374
2375        let input = TUnion::from_vec(vec![
2376            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2377            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2378        ]);
2379
2380        let options = options_with_self("Foo");
2381        let mut actual = input.clone();
2382        expand_union(&codebase, &mut actual, &options);
2383
2384        assert!(actual.types.len() <= 2);
2385    }
2386
2387    #[test]
2388    fn test_expand_deeply_nested_types() {
2389        let code = r"<?php class Foo {}";
2390        let codebase = create_test_codebase(code);
2391
2392        let inner = TList::new(Arc::new(make_self_object()));
2393        let middle = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2394        let outer = TList::new(Arc::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2395        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2396
2397        let options = options_with_self("Foo");
2398        let mut actual = input.clone();
2399        expand_union(&codebase, &mut actual, &options);
2400
2401        if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2402            && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2403            && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2404        {
2405            assert!(inner.element_type.types.iter().any(|t| {
2406                if let TAtomic::Object(TObject::Named(named)) = t {
2407                    named.name == ascii_lowercase_atom("foo")
2408                } else {
2409                    false
2410                }
2411            }));
2412        }
2413    }
2414
2415    #[test]
2416    fn test_expand_with_all_options_disabled() {
2417        let code = r"<?php class Foo {}";
2418        let codebase = create_test_codebase(code);
2419
2420        let input = make_self_object();
2421        let options = TypeExpansionOptions {
2422            self_class: None,
2423            static_class_type: StaticClassType::None,
2424            parent_class: None,
2425            evaluate_class_constants: false,
2426            evaluate_conditional_types: false,
2427            function_is_final: false,
2428            expand_generic: false,
2429            expand_templates: false,
2430        };
2431
2432        let mut actual = input.clone();
2433        expand_union(&codebase, &mut actual, &options);
2434
2435        assert!(actual.types.iter().any(|t| {
2436            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2437        }));
2438    }
2439
2440    #[test]
2441    fn test_expand_already_expanded_type() {
2442        let code = r"<?php class Foo {}";
2443        let codebase = create_test_codebase(code);
2444
2445        let input = make_named_object("Foo");
2446        let options = options_with_self("Foo");
2447
2448        let mut actual = input.clone();
2449        expand_union(&codebase, &mut actual, &options);
2450
2451        let mut actual2 = actual.clone();
2452        expand_union(&codebase, &mut actual2, &options);
2453
2454        assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2455    }
2456
2457    #[test]
2458    fn test_expand_complex_generic_class() {
2459        let code = r"<?php
2460            /**
2461             * @template T
2462             * @template U
2463             */
2464            class Container {}
2465        ";
2466        let codebase = create_test_codebase(code);
2467
2468        let named = TNamedObject::new_with_type_parameters(
2469            ascii_lowercase_atom("container"),
2470            Some(vec![make_self_object(), make_static_object()]),
2471        );
2472        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2473
2474        let options = TypeExpansionOptions {
2475            self_class: Some(ascii_lowercase_atom("foo")),
2476            static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2477            ..Default::default()
2478        };
2479
2480        let mut actual = input.clone();
2481        expand_union(&codebase, &mut actual, &options);
2482
2483        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2484            && let Some(params) = &named.type_parameters
2485        {
2486            assert!(params[0].types.iter().any(|t| {
2487                if let TAtomic::Object(TObject::Named(named)) = t {
2488                    named.name == ascii_lowercase_atom("foo")
2489                } else {
2490                    false
2491                }
2492            }));
2493            assert!(params[1].types.iter().any(|t| {
2494                if let TAtomic::Object(TObject::Named(named)) = t {
2495                    named.name == ascii_lowercase_atom("bar")
2496                } else {
2497                    false
2498                }
2499            }));
2500        }
2501    }
2502}