Skip to main content

mago_codex/ttype/
expander.rs

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