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).0;
804            let resolved_names = NameResolver::new(&arena).resolve(program);
805            let program_codebase = scan_program(&arena, &file, program, &resolved_names);
806
807            codebase.extend(program_codebase);
808        }
809
810        populate_codebase(&mut codebase, &mut SymbolReferences::new(), Default::default(), Default::default());
811
812        codebase
813    }
814
815    fn options_with_self(self_class: &str) -> TypeExpansionOptions {
816        TypeExpansionOptions { self_class: Some(ascii_lowercase_atom(self_class)), ..Default::default() }
817    }
818
819    fn options_with_static(static_class: &str) -> TypeExpansionOptions {
820        TypeExpansionOptions {
821            self_class: Some(ascii_lowercase_atom(static_class)),
822            static_class_type: StaticClassType::Name(ascii_lowercase_atom(static_class)),
823            ..Default::default()
824        }
825    }
826
827    fn options_with_static_object(object: TObject) -> TypeExpansionOptions {
828        let name = object.get_name().copied();
829        TypeExpansionOptions {
830            self_class: name,
831            static_class_type: StaticClassType::Object(object),
832            ..Default::default()
833        }
834    }
835
836    macro_rules! assert_expands_to {
837        ($codebase:expr, $input:expr, $expected:expr) => {
838            assert_expands_to!($codebase, $input, $expected, &TypeExpansionOptions::default())
839        };
840        ($codebase:expr, $input:expr, $expected:expr, $options:expr) => {{
841            let mut actual = $input.clone();
842            expand_union($codebase, &mut actual, $options);
843            assert_eq!(
844                actual.types.as_ref(),
845                $expected.types.as_ref(),
846                "Type expansion mismatch.\nInput: {:?}\nExpected: {:?}\nActual: {:?}",
847                $input,
848                $expected,
849                actual
850            );
851        }};
852    }
853
854    fn make_self_object() -> TUnion {
855        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))))
856    }
857
858    fn make_static_object() -> TUnion {
859        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))))
860    }
861
862    fn make_parent_object() -> TUnion {
863        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(atom("parent")))))
864    }
865
866    fn make_named_object(name: &str) -> TUnion {
867        TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom(name)))))
868    }
869
870    #[test]
871    fn test_expand_null_type() {
872        let codebase = CodebaseMetadata::new();
873        let null_type = get_null();
874        assert_expands_to!(&codebase, null_type, get_null());
875    }
876
877    #[test]
878    fn test_expand_void_type() {
879        let codebase = CodebaseMetadata::new();
880        let void_type = get_void();
881        assert_expands_to!(&codebase, void_type, get_void());
882    }
883
884    #[test]
885    fn test_expand_never_type() {
886        let codebase = CodebaseMetadata::new();
887        let never_type = get_never();
888        assert_expands_to!(&codebase, never_type, get_never());
889    }
890
891    #[test]
892    fn test_expand_int_type() {
893        let codebase = CodebaseMetadata::new();
894        let int_type = get_int();
895        assert_expands_to!(&codebase, int_type, get_int());
896    }
897
898    #[test]
899    fn test_expand_mixed_type() {
900        let codebase = CodebaseMetadata::new();
901        let mixed_type = get_mixed();
902        assert_expands_to!(&codebase, mixed_type, get_mixed());
903    }
904
905    #[test]
906    fn test_expand_keyed_array_with_self_key() {
907        let code = r"<?php class Foo {}";
908        let codebase = create_test_codebase(code);
909
910        let mut keyed = TKeyedArray::new();
911        keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
912        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
913
914        let options = options_with_self("Foo");
915        let mut actual = input.clone();
916        expand_union(&codebase, &mut actual, &options);
917
918        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
919            && let Some((key, _)) = &keyed.parameters
920        {
921            assert!(key.types.iter().any(|t| {
922                if let TAtomic::Object(TObject::Named(named)) = t {
923                    named.name == ascii_lowercase_atom("foo")
924                } else {
925                    false
926                }
927            }));
928        }
929    }
930
931    #[test]
932    fn test_expand_keyed_array_with_self_value() {
933        let code = r"<?php class Foo {}";
934        let codebase = create_test_codebase(code);
935
936        let mut keyed = TKeyedArray::new();
937        keyed.parameters = Some((Box::new(get_string()), Box::new(make_self_object())));
938        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
939
940        let options = options_with_self("Foo");
941        let mut actual = input.clone();
942        expand_union(&codebase, &mut actual, &options);
943
944        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
945            && let Some((_, value)) = &keyed.parameters
946        {
947            assert!(value.types.iter().any(|t| {
948                if let TAtomic::Object(TObject::Named(named)) = t {
949                    named.name == ascii_lowercase_atom("foo")
950                } else {
951                    false
952                }
953            }));
954        }
955    }
956
957    #[test]
958    fn test_expand_keyed_array_known_items() {
959        let code = r"<?php class Foo {}";
960        let codebase = create_test_codebase(code);
961
962        use crate::ttype::atomic::array::key::ArrayKey;
963        use std::collections::BTreeMap;
964
965        let mut keyed = TKeyedArray::new();
966        let mut known_items = BTreeMap::new();
967        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
968        keyed.known_items = Some(known_items);
969        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
970
971        let options = options_with_self("Foo");
972        let mut actual = input.clone();
973        expand_union(&codebase, &mut actual, &options);
974
975        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
976            && let Some(items) = &keyed.known_items
977        {
978            let (_, item_type) = items.get(&ArrayKey::String(atom("key"))).unwrap();
979            assert!(item_type.types.iter().any(|t| {
980                if let TAtomic::Object(TObject::Named(named)) = t {
981                    named.name == ascii_lowercase_atom("foo")
982                } else {
983                    false
984                }
985            }));
986        }
987    }
988
989    #[test]
990    fn test_expand_list_with_self_element() {
991        let code = r"<?php class Foo {}";
992        let codebase = create_test_codebase(code);
993
994        let list = TList::new(Box::new(make_self_object()));
995        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
996
997        let options = options_with_self("Foo");
998        let mut actual = input.clone();
999        expand_union(&codebase, &mut actual, &options);
1000
1001        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1002            assert!(list.element_type.types.iter().any(|t| {
1003                if let TAtomic::Object(TObject::Named(named)) = t {
1004                    named.name == ascii_lowercase_atom("foo")
1005                } else {
1006                    false
1007                }
1008            }));
1009        }
1010    }
1011
1012    #[test]
1013    fn test_expand_list_known_elements() {
1014        let code = r"<?php class Foo {}";
1015        let codebase = create_test_codebase(code);
1016
1017        use std::collections::BTreeMap;
1018
1019        let mut list = TList::new(Box::new(get_mixed()));
1020        let mut known_elements = BTreeMap::new();
1021        known_elements.insert(0, (false, make_self_object()));
1022        list.known_elements = Some(known_elements);
1023        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1024
1025        let options = options_with_self("Foo");
1026        let mut actual = input.clone();
1027        expand_union(&codebase, &mut actual, &options);
1028
1029        if let TAtomic::Array(TArray::List(list)) = &actual.types[0]
1030            && let Some(elements) = &list.known_elements
1031        {
1032            let (_, element_type) = elements.get(&0).unwrap();
1033            assert!(element_type.types.iter().any(|t| {
1034                if let TAtomic::Object(TObject::Named(named)) = t {
1035                    named.name == ascii_lowercase_atom("foo")
1036                } else {
1037                    false
1038                }
1039            }));
1040        }
1041    }
1042
1043    #[test]
1044    fn test_expand_nested_array() {
1045        let code = r"<?php class Foo {}";
1046        let codebase = create_test_codebase(code);
1047
1048        let inner_list = TList::new(Box::new(make_self_object()));
1049        let inner_array = TUnion::from_atomic(TAtomic::Array(TArray::List(inner_list)));
1050
1051        let mut outer = TKeyedArray::new();
1052        outer.parameters = Some((Box::new(make_self_object()), Box::new(inner_array)));
1053        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(outer)));
1054
1055        let options = options_with_self("Foo");
1056        let mut actual = input.clone();
1057        expand_union(&codebase, &mut actual, &options);
1058
1059        if let TAtomic::Array(TArray::Keyed(keyed)) = &actual.types[0]
1060            && let Some((key, value)) = &keyed.parameters
1061        {
1062            assert!(key.types.iter().any(|t| {
1063                if let TAtomic::Object(TObject::Named(named)) = t {
1064                    named.name == ascii_lowercase_atom("foo")
1065                } else {
1066                    false
1067                }
1068            }));
1069            if let TAtomic::Array(TArray::List(inner)) = &value.types[0] {
1070                assert!(inner.element_type.types.iter().any(|t| {
1071                    if let TAtomic::Object(TObject::Named(named)) = t {
1072                        named.name == ascii_lowercase_atom("foo")
1073                    } else {
1074                        false
1075                    }
1076                }));
1077            }
1078        }
1079    }
1080
1081    #[test]
1082    fn test_expand_empty_array() {
1083        let codebase = CodebaseMetadata::new();
1084        let keyed = TKeyedArray::new();
1085        let input = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed.clone())));
1086        let expected = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1087        assert_expands_to!(&codebase, input, expected);
1088    }
1089
1090    #[test]
1091    fn test_expand_non_empty_list() {
1092        let code = r"<?php class Foo {}";
1093        let codebase = create_test_codebase(code);
1094
1095        let mut list = TList::new(Box::new(make_self_object()));
1096        list.non_empty = true;
1097        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(list)));
1098
1099        let options = options_with_self("Foo");
1100        let mut actual = input.clone();
1101        expand_union(&codebase, &mut actual, &options);
1102
1103        if let TAtomic::Array(TArray::List(list)) = &actual.types[0] {
1104            assert!(list.non_empty);
1105            assert!(list.element_type.types.iter().any(|t| {
1106                if let TAtomic::Object(TObject::Named(named)) = t {
1107                    named.name == ascii_lowercase_atom("foo")
1108                } else {
1109                    false
1110                }
1111            }));
1112        }
1113    }
1114
1115    #[test]
1116    fn test_expand_self_to_class_name() {
1117        let code = r"<?php class Foo {}";
1118        let codebase = create_test_codebase(code);
1119
1120        let input = make_self_object();
1121        let options = options_with_self("Foo");
1122        let mut actual = input.clone();
1123        expand_union(&codebase, &mut actual, &options);
1124
1125        assert!(actual.types.iter().any(|t| {
1126            if let TAtomic::Object(TObject::Named(named)) = t {
1127                named.name == ascii_lowercase_atom("foo")
1128            } else {
1129                false
1130            }
1131        }));
1132    }
1133
1134    #[test]
1135    fn test_expand_static_to_class_name() {
1136        let code = r"<?php class Foo {}";
1137        let codebase = create_test_codebase(code);
1138
1139        let input = make_static_object();
1140        let options = options_with_static("Foo");
1141        let mut actual = input.clone();
1142        expand_union(&codebase, &mut actual, &options);
1143
1144        assert!(actual.types.iter().any(|t| {
1145            if let TAtomic::Object(TObject::Named(named)) = t {
1146                named.name == ascii_lowercase_atom("foo")
1147            } else {
1148                false
1149            }
1150        }));
1151    }
1152
1153    #[test]
1154    fn test_expand_static_with_object_type() {
1155        let code = r"<?php class Foo {}";
1156        let codebase = create_test_codebase(code);
1157
1158        let input = make_static_object();
1159        let static_obj = TObject::Named(TNamedObject::new(ascii_lowercase_atom("foo")));
1160        let options = options_with_static_object(static_obj);
1161        let mut actual = input.clone();
1162        expand_union(&codebase, &mut actual, &options);
1163
1164        assert!(actual.types.iter().any(|t| {
1165            if let TAtomic::Object(TObject::Named(named)) = t {
1166                named.name == ascii_lowercase_atom("foo") && named.is_this
1167            } else {
1168                false
1169            }
1170        }));
1171    }
1172
1173    #[test]
1174    fn test_expand_static_with_enum_type() {
1175        let code = r"<?php enum Status { case Active; case Inactive; }";
1176        let codebase = create_test_codebase(code);
1177
1178        let input = make_static_object();
1179        let static_enum = TObject::Enum(TEnum::new(ascii_lowercase_atom("status")));
1180        let options = options_with_static_object(static_enum);
1181        let mut actual = input.clone();
1182        expand_union(&codebase, &mut actual, &options);
1183
1184        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1185    }
1186
1187    #[test]
1188    fn test_expand_parent_to_parent_class() {
1189        let code = r"<?php
1190            class BaseClass {}
1191            class ChildClass extends BaseClass {}
1192        ";
1193        let codebase = create_test_codebase(code);
1194
1195        let input = make_parent_object();
1196        let options = options_with_self("ChildClass");
1197        let mut actual = input.clone();
1198        expand_union(&codebase, &mut actual, &options);
1199
1200        assert!(actual.types.iter().any(|t| {
1201            if let TAtomic::Object(TObject::Named(named)) = t {
1202                named.name == ascii_lowercase_atom("baseclass")
1203            } else {
1204                false
1205            }
1206        }));
1207    }
1208
1209    #[test]
1210    fn test_expand_parent_without_parent_class() {
1211        let code = r"<?php class Foo {}";
1212        let codebase = create_test_codebase(code);
1213
1214        let input = make_parent_object();
1215        let options = options_with_self("Foo");
1216        let mut actual = input.clone();
1217        expand_union(&codebase, &mut actual, &options);
1218
1219        assert!(actual.types.iter().any(|t| {
1220            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("parent") } else { false }
1221        }));
1222    }
1223
1224    #[test]
1225    fn test_expand_this_variable() {
1226        let code = r"<?php class Foo {}";
1227        let codebase = create_test_codebase(code);
1228
1229        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(TNamedObject::new_this(atom("$this")))));
1230        let options = options_with_static("Foo");
1231        let mut actual = input.clone();
1232        expand_union(&codebase, &mut actual, &options);
1233
1234        assert!(actual.types.iter().any(|t| {
1235            if let TAtomic::Object(TObject::Named(named)) = t {
1236                named.name == ascii_lowercase_atom("foo")
1237            } else {
1238                false
1239            }
1240        }));
1241    }
1242
1243    #[test]
1244    fn test_expand_this_with_final_function() {
1245        let code = r"<?php class Foo {}";
1246        let codebase = create_test_codebase(code);
1247
1248        let input = make_static_object();
1249        let options = TypeExpansionOptions {
1250            self_class: Some(ascii_lowercase_atom("foo")),
1251            static_class_type: StaticClassType::Name(ascii_lowercase_atom("foo")),
1252            function_is_final: true,
1253            ..Default::default()
1254        };
1255        let mut actual = input.clone();
1256        expand_union(&codebase, &mut actual, &options);
1257
1258        assert!(actual.types.iter().any(|t| {
1259            if let TAtomic::Object(TObject::Named(named)) = t {
1260                named.name == ascii_lowercase_atom("foo") && !named.is_this
1261            } else {
1262                false
1263            }
1264        }));
1265    }
1266
1267    #[test]
1268    fn test_expand_object_with_type_parameters() {
1269        let code = r"<?php class Container {}";
1270        let codebase = create_test_codebase(code);
1271
1272        let named =
1273            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1274        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1275
1276        let options = options_with_self("Foo");
1277        let mut actual = input.clone();
1278        expand_union(&codebase, &mut actual, &options);
1279
1280        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
1281            && let Some(params) = &named.type_parameters
1282        {
1283            assert!(params[0].types.iter().any(|t| {
1284                if let TAtomic::Object(TObject::Named(named)) = t {
1285                    named.name == ascii_lowercase_atom("foo")
1286                } else {
1287                    false
1288                }
1289            }));
1290        }
1291    }
1292
1293    #[test]
1294    fn test_expand_object_gets_default_type_params() {
1295        let code = r"<?php
1296            /** @template T */
1297            class Container {}
1298        ";
1299        let codebase = create_test_codebase(code);
1300
1301        let named = TNamedObject::new(ascii_lowercase_atom("container"));
1302        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
1303
1304        let mut actual = input.clone();
1305        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1306
1307        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1308            assert!(named.type_parameters.is_some());
1309        }
1310    }
1311
1312    #[test]
1313    fn test_expand_object_intersection_from_static() {
1314        let code = r"<?php
1315            interface Stringable {}
1316            class Foo implements Stringable {}
1317        ";
1318        let codebase = create_test_codebase(code);
1319
1320        let input = make_static_object();
1321
1322        let mut static_named = TNamedObject::new(ascii_lowercase_atom("foo"));
1323        static_named.intersection_types =
1324            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1325        let static_obj = TObject::Named(static_named);
1326        let options = options_with_static_object(static_obj);
1327
1328        let mut actual = input.clone();
1329        expand_union(&codebase, &mut actual, &options);
1330
1331        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0] {
1332            assert!(named.intersection_types.is_some());
1333        }
1334    }
1335
1336    #[test]
1337    fn test_expand_self_without_self_class_option() {
1338        let codebase = CodebaseMetadata::new();
1339
1340        let input = make_self_object();
1341        let mut actual = input.clone();
1342        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1343
1344        assert!(actual.types.iter().any(|t| {
1345            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
1346        }));
1347    }
1348
1349    #[test]
1350    fn test_expand_callable_return_type() {
1351        let code = r"<?php class Foo {}";
1352        let codebase = create_test_codebase(code);
1353
1354        let sig = TCallableSignature::new(false, false).with_return_type(Some(Box::new(make_self_object())));
1355        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1356
1357        let options = options_with_self("Foo");
1358        let mut actual = input.clone();
1359        expand_union(&codebase, &mut actual, &options);
1360
1361        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1362            && let Some(ret) = sig.get_return_type()
1363        {
1364            assert!(ret.types.iter().any(|t| {
1365                if let TAtomic::Object(TObject::Named(named)) = t {
1366                    named.name == ascii_lowercase_atom("foo")
1367                } else {
1368                    false
1369                }
1370            }));
1371        }
1372    }
1373
1374    #[test]
1375    fn test_expand_callable_parameter_types() {
1376        let code = r"<?php class Foo {}";
1377        let codebase = create_test_codebase(code);
1378
1379        let param = TCallableParameter::new(Some(Box::new(make_self_object())), false, false, false);
1380        let sig = TCallableSignature::new(false, false).with_parameters(vec![param]);
1381        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1382
1383        let options = options_with_self("Foo");
1384        let mut actual = input.clone();
1385        expand_union(&codebase, &mut actual, &options);
1386
1387        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1388            && let Some(param) = sig.get_parameters().first()
1389            && let Some(param_type) = param.get_type_signature()
1390        {
1391            assert!(param_type.types.iter().any(|t| {
1392                if let TAtomic::Object(TObject::Named(named)) = t {
1393                    named.name == ascii_lowercase_atom("foo")
1394                } else {
1395                    false
1396                }
1397            }));
1398        }
1399    }
1400
1401    #[test]
1402    fn test_expand_callable_alias_to_function() {
1403        let code = r"<?php
1404            function myFunc(): int { return 1; }
1405        ";
1406        let codebase = create_test_codebase(code);
1407
1408        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc")));
1409        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1410
1411        let mut actual = input.clone();
1412        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1413
1414        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1415    }
1416
1417    #[test]
1418    fn test_expand_callable_alias_to_method() {
1419        let code = r"<?php
1420            class Foo {
1421                public function bar(): int { return 1; }
1422            }
1423        ";
1424        let codebase = create_test_codebase(code);
1425
1426        let alias =
1427            TCallable::Alias(FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar")));
1428        let input = TUnion::from_atomic(TAtomic::Callable(alias));
1429
1430        let mut actual = input.clone();
1431        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1432
1433        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Signature(_)))));
1434    }
1435
1436    #[test]
1437    fn test_expand_callable_alias_unknown() {
1438        let codebase = CodebaseMetadata::new();
1439
1440        let alias = TCallable::Alias(FunctionLikeIdentifier::Function(atom("nonexistent")));
1441        let input = TUnion::from_atomic(TAtomic::Callable(alias.clone()));
1442
1443        let mut actual = input.clone();
1444        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1445
1446        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Callable(TCallable::Alias(_)))));
1447    }
1448
1449    #[test]
1450    fn test_expand_closure_signature() {
1451        let code = r"<?php class Foo {}";
1452        let codebase = create_test_codebase(code);
1453
1454        let sig = TCallableSignature::new(false, true).with_return_type(Some(Box::new(make_self_object())));
1455        let input = TUnion::from_atomic(TAtomic::Callable(TCallable::Signature(sig)));
1456
1457        let options = options_with_self("Foo");
1458        let mut actual = input.clone();
1459        expand_union(&codebase, &mut actual, &options);
1460
1461        if let TAtomic::Callable(TCallable::Signature(sig)) = &actual.types[0]
1462            && let Some(ret) = sig.get_return_type()
1463        {
1464            assert!(ret.types.iter().any(|t| {
1465                if let TAtomic::Object(TObject::Named(named)) = t {
1466                    named.name == ascii_lowercase_atom("foo")
1467                } else {
1468                    false
1469                }
1470            }));
1471        }
1472    }
1473
1474    #[test]
1475    fn test_expand_generic_parameter_constraint() {
1476        let code = r"<?php class Foo {}";
1477        let codebase = create_test_codebase(code);
1478
1479        let generic = TGenericParameter::new(
1480            atom("T"),
1481            Box::new(make_self_object()),
1482            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1483        );
1484        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1485
1486        let options = options_with_self("Foo");
1487        let mut actual = input.clone();
1488        expand_union(&codebase, &mut actual, &options);
1489
1490        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1491            assert!(param.constraint.types.iter().any(|t| {
1492                if let TAtomic::Object(TObject::Named(named)) = t {
1493                    named.name == ascii_lowercase_atom("foo")
1494                } else {
1495                    false
1496                }
1497            }));
1498        }
1499    }
1500
1501    #[test]
1502    fn test_expand_nested_generic_constraint() {
1503        let code = r"<?php class Foo {} class Bar {}";
1504        let codebase = create_test_codebase(code);
1505
1506        let container =
1507            TNamedObject::new_with_type_parameters(ascii_lowercase_atom("container"), Some(vec![make_self_object()]));
1508        let constraint = TUnion::from_atomic(TAtomic::Object(TObject::Named(container)));
1509
1510        let generic = TGenericParameter::new(
1511            atom("T"),
1512            Box::new(constraint),
1513            GenericParent::ClassLike(ascii_lowercase_atom("bar")),
1514        );
1515        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1516
1517        let options = options_with_self("Foo");
1518        let mut actual = input.clone();
1519        expand_union(&codebase, &mut actual, &options);
1520
1521        if let TAtomic::GenericParameter(param) = &actual.types[0]
1522            && let TAtomic::Object(TObject::Named(named)) = &param.constraint.types[0]
1523            && let Some(params) = &named.type_parameters
1524        {
1525            assert!(params[0].types.iter().any(|t| {
1526                if let TAtomic::Object(TObject::Named(named)) = t {
1527                    named.name == ascii_lowercase_atom("foo")
1528                } else {
1529                    false
1530                }
1531            }));
1532        }
1533    }
1534
1535    #[test]
1536    fn test_expand_generic_with_intersection() {
1537        let code = r"<?php
1538            interface Stringable {}
1539            class Foo {}
1540        ";
1541        let codebase = create_test_codebase(code);
1542
1543        let mut generic = TGenericParameter::new(
1544            atom("T"),
1545            Box::new(make_self_object()),
1546            GenericParent::ClassLike(ascii_lowercase_atom("foo")),
1547        );
1548        generic.intersection_types =
1549            Some(vec![TAtomic::Object(TObject::Named(TNamedObject::new(ascii_lowercase_atom("stringable"))))]);
1550        let input = TUnion::from_atomic(TAtomic::GenericParameter(generic));
1551
1552        let options = options_with_self("Foo");
1553        let mut actual = input.clone();
1554        expand_union(&codebase, &mut actual, &options);
1555
1556        if let TAtomic::GenericParameter(param) = &actual.types[0] {
1557            assert!(param.intersection_types.is_some());
1558            assert!(param.constraint.types.iter().any(|t| {
1559                if let TAtomic::Object(TObject::Named(named)) = t {
1560                    named.name == ascii_lowercase_atom("foo")
1561                } else {
1562                    false
1563                }
1564            }));
1565        }
1566    }
1567
1568    #[test]
1569    fn test_expand_class_string_of_self() {
1570        let code = r"<?php class Foo {}";
1571        let codebase = create_test_codebase(code);
1572
1573        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1574        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1575        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1576
1577        let options = options_with_self("Foo");
1578        let mut actual = input.clone();
1579        expand_union(&codebase, &mut actual, &options);
1580
1581        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1582            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1583        {
1584            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1585        }
1586    }
1587
1588    #[test]
1589    fn test_expand_class_string_of_static() {
1590        let code = r"<?php class Foo {}";
1591        let codebase = create_test_codebase(code);
1592
1593        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("static")))));
1594        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Class, constraint };
1595        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1596
1597        let options = options_with_static("Foo");
1598        let mut actual = input.clone();
1599        expand_union(&codebase, &mut actual, &options);
1600
1601        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { constraint, .. })) = &actual.types[0]
1602            && let TAtomic::Object(TObject::Named(named)) = constraint.as_ref()
1603        {
1604            assert_eq!(named.name, ascii_lowercase_atom("foo"));
1605        }
1606    }
1607
1608    #[test]
1609    fn test_expand_interface_string_of_type() {
1610        let code = r"<?php interface MyInterface {}";
1611        let codebase = create_test_codebase(code);
1612
1613        let constraint = Box::new(TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))));
1614        let class_string = TClassLikeString::OfType { kind: TClassLikeStringKind::Interface, constraint };
1615        let input = TUnion::from_atomic(TAtomic::Scalar(TScalar::ClassLikeString(class_string)));
1616
1617        let options = options_with_self("MyInterface");
1618        let mut actual = input.clone();
1619        expand_union(&codebase, &mut actual, &options);
1620
1621        if let TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::OfType { kind, constraint })) =
1622            &actual.types[0]
1623        {
1624            assert!(matches!(kind, TClassLikeStringKind::Interface));
1625            if let TAtomic::Object(TObject::Named(named)) = constraint.as_ref() {
1626                assert_eq!(named.name, ascii_lowercase_atom("myinterface"));
1627            }
1628        }
1629    }
1630
1631    #[test]
1632    fn test_expand_member_reference_wildcard_constants() {
1633        let code = r"<?php
1634            class Foo {
1635                public const A = 1;
1636                public const B = 2;
1637            }
1638        ";
1639        let codebase = create_test_codebase(code);
1640
1641        let reference = TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Wildcard);
1642        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1643
1644        let mut actual = input.clone();
1645        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1646
1647        assert!(!actual.types.is_empty());
1648    }
1649
1650    #[test]
1651    fn test_expand_member_reference_wildcard_enum_cases() {
1652        let code = r"<?php
1653            enum Status {
1654                case Active;
1655                case Inactive;
1656            }
1657        ";
1658        let codebase = create_test_codebase(code);
1659
1660        let reference = TReference::new_member(ascii_lowercase_atom("status"), TReferenceMemberSelector::Wildcard);
1661        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1662
1663        let mut actual = input.clone();
1664        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1665
1666        assert_eq!(actual.types.len(), 2);
1667        assert!(actual.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_)))));
1668    }
1669
1670    #[test]
1671    fn test_expand_member_reference_starts_with() {
1672        let code = r"<?php
1673            class Foo {
1674                public const STATUS_ACTIVE = 1;
1675                public const STATUS_INACTIVE = 2;
1676                public const OTHER = 3;
1677            }
1678        ";
1679        let codebase = create_test_codebase(code);
1680
1681        let reference =
1682            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::StartsWith(atom("STATUS_")));
1683        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1684
1685        let mut actual = input.clone();
1686        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1687
1688        assert!(!actual.types.is_empty());
1689    }
1690
1691    #[test]
1692    fn test_expand_member_reference_ends_with() {
1693        let code = r"<?php
1694            class Foo {
1695                public const READ_ERROR = 1;
1696                public const WRITE_ERROR = 2;
1697                public const SUCCESS = 0;
1698            }
1699        ";
1700        let codebase = create_test_codebase(code);
1701
1702        let reference =
1703            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::EndsWith(atom("_ERROR")));
1704        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1705
1706        let mut actual = input.clone();
1707        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1708
1709        assert!(!actual.types.is_empty());
1710    }
1711
1712    #[test]
1713    fn test_expand_member_reference_identifier_constant() {
1714        let code = r"<?php
1715            class Foo {
1716                public const BAR = 42;
1717            }
1718        ";
1719        let codebase = create_test_codebase(code);
1720
1721        let reference =
1722            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("BAR")));
1723        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1724
1725        let mut actual = input.clone();
1726        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1727
1728        assert_eq!(actual.types.len(), 1);
1729    }
1730
1731    #[test]
1732    fn test_expand_member_reference_identifier_enum_case() {
1733        let code = r"<?php
1734            enum Status {
1735                case Active;
1736            }
1737        ";
1738        let codebase = create_test_codebase(code);
1739
1740        let reference = TReference::new_member(
1741            ascii_lowercase_atom("status"),
1742            TReferenceMemberSelector::Identifier(atom("Active")),
1743        );
1744        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1745
1746        let mut actual = input.clone();
1747        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1748
1749        assert_eq!(actual.types.len(), 1);
1750        assert!(matches!(&actual.types[0], TAtomic::Object(TObject::Enum(_))));
1751    }
1752
1753    #[test]
1754    fn test_expand_member_reference_unknown_class() {
1755        let codebase = CodebaseMetadata::new();
1756
1757        let reference = TReference::new_member(atom("NonExistent"), TReferenceMemberSelector::Identifier(atom("FOO")));
1758        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1759
1760        let mut actual = input.clone();
1761        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1762
1763        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1764    }
1765
1766    #[test]
1767    fn test_expand_member_reference_unknown_member() {
1768        let code = r"<?php class Foo {}";
1769        let codebase = create_test_codebase(code);
1770
1771        let reference = TReference::new_member(
1772            ascii_lowercase_atom("foo"),
1773            TReferenceMemberSelector::Identifier(atom("NONEXISTENT")),
1774        );
1775        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1776
1777        let mut actual = input.clone();
1778        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1779
1780        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))));
1781    }
1782
1783    #[test]
1784    fn test_expand_member_reference_constant_with_inferred_type() {
1785        let code = r#"<?php
1786            class Foo {
1787                public const VALUE = "hello";
1788            }
1789        "#;
1790        let codebase = create_test_codebase(code);
1791
1792        let reference =
1793            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1794        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1795
1796        let mut actual = input.clone();
1797        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1798
1799        assert_eq!(actual.types.len(), 1);
1800    }
1801
1802    #[test]
1803    fn test_expand_member_reference_constant_with_type_metadata() {
1804        let code = r"<?php
1805            class Foo {
1806                /** @var int */
1807                public const VALUE = 42;
1808            }
1809        ";
1810        let codebase = create_test_codebase(code);
1811
1812        let reference =
1813            TReference::new_member(ascii_lowercase_atom("foo"), TReferenceMemberSelector::Identifier(atom("VALUE")));
1814        let input = TUnion::from_atomic(TAtomic::Reference(reference));
1815
1816        let mut actual = input.clone();
1817        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1818
1819        assert_eq!(actual.types.len(), 1);
1820    }
1821
1822    #[test]
1823    fn test_expand_conditional_both_branches() {
1824        let code = r"<?php class Foo {} class Bar {}";
1825        let codebase = create_test_codebase(code);
1826
1827        let conditional = TConditional::new(
1828            Box::new(get_mixed()),
1829            Box::new(get_string()),
1830            Box::new(make_self_object()),
1831            Box::new(make_self_object()),
1832            false,
1833        );
1834        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1835
1836        let options = options_with_self("Foo");
1837        let mut actual = input.clone();
1838        expand_union(&codebase, &mut actual, &options);
1839
1840        assert!(actual.types.iter().any(|t| {
1841            if let TAtomic::Object(TObject::Named(named)) = t {
1842                named.name == ascii_lowercase_atom("foo")
1843            } else {
1844                false
1845            }
1846        }));
1847    }
1848
1849    #[test]
1850    fn test_expand_conditional_with_self_in_then() {
1851        let code = r"<?php class Foo {}";
1852        let codebase = create_test_codebase(code);
1853
1854        let conditional = TConditional::new(
1855            Box::new(get_mixed()),
1856            Box::new(get_string()),
1857            Box::new(make_self_object()),
1858            Box::new(get_int()),
1859            false,
1860        );
1861        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1862
1863        let options = options_with_self("Foo");
1864        let mut actual = input.clone();
1865        expand_union(&codebase, &mut actual, &options);
1866
1867        assert!(!actual.types.is_empty());
1868    }
1869
1870    #[test]
1871    fn test_expand_conditional_with_self_in_otherwise() {
1872        let code = r"<?php class Foo {}";
1873        let codebase = create_test_codebase(code);
1874
1875        let conditional = TConditional::new(
1876            Box::new(get_mixed()),
1877            Box::new(get_string()),
1878            Box::new(get_int()),
1879            Box::new(make_self_object()),
1880            false,
1881        );
1882        let input = TUnion::from_atomic(TAtomic::Conditional(conditional));
1883
1884        let options = options_with_self("Foo");
1885        let mut actual = input.clone();
1886        expand_union(&codebase, &mut actual, &options);
1887
1888        assert!(!actual.types.is_empty());
1889    }
1890
1891    #[test]
1892    fn test_expand_simple_alias() {
1893        let code = r"<?php
1894            class Foo {
1895                /** @phpstan-type MyInt = int */
1896            }
1897        ";
1898        let codebase = create_test_codebase(code);
1899
1900        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MyInt"));
1901        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1902
1903        let mut actual = input.clone();
1904        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1905
1906        assert!(!actual.types.is_empty());
1907    }
1908
1909    #[test]
1910    fn test_expand_nested_alias() {
1911        let code = r"<?php
1912            class Foo {
1913                /** @phpstan-type Inner = int */
1914                /** @phpstan-type Outer = Inner */
1915            }
1916        ";
1917        let codebase = create_test_codebase(code);
1918
1919        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("Outer"));
1920        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1921
1922        let mut actual = input.clone();
1923        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1924
1925        assert!(!actual.types.is_empty());
1926    }
1927
1928    #[test]
1929    fn test_expand_alias_cycle_detection() {
1930        let codebase = CodebaseMetadata::new();
1931
1932        let alias = TAlias::new(atom("Foo"), atom("SelfRef"));
1933        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1934
1935        let mut actual = input.clone();
1936        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1937
1938        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1939    }
1940
1941    #[test]
1942    fn test_expand_alias_unknown() {
1943        let codebase = CodebaseMetadata::new();
1944
1945        let alias = TAlias::new(atom("NonExistent"), atom("Unknown"));
1946        let input = TUnion::from_atomic(TAtomic::Alias(alias.clone()));
1947
1948        let mut actual = input.clone();
1949        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1950
1951        assert!(actual.types.iter().any(|t| matches!(t, TAtomic::Alias(_))));
1952    }
1953
1954    #[test]
1955    fn test_expand_alias_with_self_inside() {
1956        let code = r"<?php
1957            class Foo {
1958                /** @phpstan-type MySelf = self */
1959            }
1960        ";
1961        let codebase = create_test_codebase(code);
1962
1963        let alias = TAlias::new(ascii_lowercase_atom("foo"), atom("MySelf"));
1964        let input = TUnion::from_atomic(TAtomic::Alias(alias));
1965
1966        let options = options_with_self("Foo");
1967        let mut actual = input.clone();
1968        expand_union(&codebase, &mut actual, &options);
1969
1970        assert!(!actual.types.is_empty());
1971    }
1972
1973    #[test]
1974    fn test_expand_key_of_array() {
1975        let codebase = CodebaseMetadata::new();
1976
1977        let mut keyed = TKeyedArray::new();
1978        keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
1979        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1980
1981        let key_of = TKeyOf::new(Box::new(array_type));
1982        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
1983
1984        let mut actual = input.clone();
1985        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
1986
1987        assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_string));
1988    }
1989
1990    #[test]
1991    fn test_expand_key_of_with_self() {
1992        let code = r"<?php class Foo {}";
1993        let codebase = create_test_codebase(code);
1994
1995        let mut keyed = TKeyedArray::new();
1996        keyed.parameters = Some((Box::new(make_self_object()), Box::new(get_int())));
1997        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
1998
1999        let key_of = TKeyOf::new(Box::new(array_type));
2000        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::KeyOf(key_of)));
2001
2002        let options = options_with_self("Foo");
2003        let mut actual = input.clone();
2004        expand_union(&codebase, &mut actual, &options);
2005
2006        assert!(!actual.types.is_empty());
2007    }
2008
2009    #[test]
2010    fn test_expand_value_of_array() {
2011        let codebase = CodebaseMetadata::new();
2012
2013        let mut keyed = TKeyedArray::new();
2014        keyed.parameters = Some((Box::new(get_string()), Box::new(get_int())));
2015        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2016
2017        let value_of = TValueOf::new(Box::new(array_type));
2018        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2019
2020        let mut actual = input.clone();
2021        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2022
2023        assert!(actual.types.iter().any(super::super::atomic::TAtomic::is_int));
2024    }
2025
2026    #[test]
2027    fn test_expand_value_of_enum() {
2028        let code = r"<?php
2029            enum Status: string {
2030                case Active = 'active';
2031                case Inactive = 'inactive';
2032            }
2033        ";
2034        let codebase = create_test_codebase(code);
2035
2036        let enum_type = TUnion::from_atomic(TAtomic::Object(TObject::Enum(TEnum::new(ascii_lowercase_atom("status")))));
2037
2038        let value_of = TValueOf::new(Box::new(enum_type));
2039        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::ValueOf(value_of)));
2040
2041        let mut actual = input.clone();
2042        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2043
2044        assert!(!actual.types.is_empty());
2045    }
2046
2047    #[test]
2048    fn test_expand_index_access() {
2049        let codebase = CodebaseMetadata::new();
2050
2051        use crate::ttype::atomic::array::key::ArrayKey;
2052        use std::collections::BTreeMap;
2053
2054        let mut keyed = TKeyedArray::new();
2055        let mut known_items = BTreeMap::new();
2056        known_items.insert(ArrayKey::String(atom("key")), (false, get_int()));
2057        keyed.known_items = Some(known_items);
2058        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2059
2060        use crate::ttype::get_literal_string;
2061        let index_type = get_literal_string(atom("key"));
2062
2063        let index_access = TIndexAccess::new(array_type, index_type);
2064        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2065
2066        let mut actual = input.clone();
2067        expand_union(&codebase, &mut actual, &TypeExpansionOptions::default());
2068
2069        assert!(!actual.types.is_empty());
2070    }
2071
2072    #[test]
2073    fn test_expand_index_access_with_self() {
2074        let code = r"<?php class Foo {}";
2075        let codebase = create_test_codebase(code);
2076
2077        use crate::ttype::atomic::array::key::ArrayKey;
2078        use std::collections::BTreeMap;
2079
2080        let mut keyed = TKeyedArray::new();
2081        let mut known_items = BTreeMap::new();
2082        known_items.insert(ArrayKey::String(atom("key")), (false, make_self_object()));
2083        keyed.known_items = Some(known_items);
2084        let array_type = TUnion::from_atomic(TAtomic::Array(TArray::Keyed(keyed)));
2085
2086        use crate::ttype::get_literal_string;
2087        let index_type = get_literal_string(atom("key"));
2088
2089        let index_access = TIndexAccess::new(array_type, index_type);
2090        let input = TUnion::from_atomic(TAtomic::Derived(TDerived::IndexAccess(index_access)));
2091
2092        let options = options_with_self("Foo");
2093        let mut actual = input.clone();
2094        expand_union(&codebase, &mut actual, &options);
2095
2096        assert!(!actual.types.is_empty());
2097    }
2098
2099    #[test]
2100    fn test_expand_iterable_key_type() {
2101        let code = r"<?php class Foo {}";
2102        let codebase = create_test_codebase(code);
2103
2104        let iterable = TIterable::new(Box::new(make_self_object()), Box::new(get_int()));
2105        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2106
2107        let options = options_with_self("Foo");
2108        let mut actual = input.clone();
2109        expand_union(&codebase, &mut actual, &options);
2110
2111        if let TAtomic::Iterable(iter) = &actual.types[0] {
2112            assert!(iter.get_key_type().types.iter().any(|t| {
2113                if let TAtomic::Object(TObject::Named(named)) = t {
2114                    named.name == ascii_lowercase_atom("foo")
2115                } else {
2116                    false
2117                }
2118            }));
2119        }
2120    }
2121
2122    #[test]
2123    fn test_expand_iterable_value_type() {
2124        let code = r"<?php class Foo {}";
2125        let codebase = create_test_codebase(code);
2126
2127        let iterable = TIterable::new(Box::new(get_int()), Box::new(make_self_object()));
2128        let input = TUnion::from_atomic(TAtomic::Iterable(iterable));
2129
2130        let options = options_with_self("Foo");
2131        let mut actual = input.clone();
2132        expand_union(&codebase, &mut actual, &options);
2133
2134        if let TAtomic::Iterable(iter) = &actual.types[0] {
2135            assert!(iter.get_value_type().types.iter().any(|t| {
2136                if let TAtomic::Object(TObject::Named(named)) = t {
2137                    named.name == ascii_lowercase_atom("foo")
2138                } else {
2139                    false
2140                }
2141            }));
2142        }
2143    }
2144
2145    #[test]
2146    fn test_get_signature_of_function() {
2147        let code = r#"<?php
2148            function myFunc(int $a): string { return ""; }
2149        "#;
2150        let codebase = create_test_codebase(code);
2151
2152        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2153
2154        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2155        assert!(sig.is_some());
2156
2157        let sig = sig.unwrap();
2158        assert_eq!(sig.get_parameters().len(), 1);
2159        assert!(sig.get_return_type().is_some());
2160    }
2161
2162    #[test]
2163    fn test_get_signature_of_method() {
2164        let code = r"<?php
2165            class Foo {
2166                public function bar(string $s): int { return 0; }
2167            }
2168        ";
2169        let codebase = create_test_codebase(code);
2170
2171        let id = FunctionLikeIdentifier::Method(ascii_lowercase_atom("foo"), ascii_lowercase_atom("bar"));
2172
2173        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2174        assert!(sig.is_some());
2175
2176        let sig = sig.unwrap();
2177        assert_eq!(sig.get_parameters().len(), 1);
2178    }
2179
2180    #[test]
2181    fn test_get_signature_of_closure() {
2182        let codebase = CodebaseMetadata::new();
2183
2184        let id = FunctionLikeIdentifier::Closure(FileId::new("test"), Position::new(0));
2185        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2186
2187        assert!(sig.is_none());
2188    }
2189
2190    #[test]
2191    fn test_get_atomic_of_function() {
2192        let code = r"<?php
2193            function myFunc(): void {}
2194        ";
2195        let codebase = create_test_codebase(code);
2196
2197        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("myfunc"));
2198
2199        let atomic = get_atomic_of_function_like_identifier(&id, &codebase);
2200        assert!(atomic.is_some());
2201        assert!(matches!(atomic.unwrap(), TAtomic::Callable(TCallable::Signature(_))));
2202    }
2203
2204    #[test]
2205    fn test_get_signature_with_parameters() {
2206        let code = r"<?php
2207            function multiParam(int $a, string $b, ?float $c = null): bool { return true; }
2208        ";
2209        let codebase = create_test_codebase(code);
2210
2211        let id = FunctionLikeIdentifier::Function(ascii_lowercase_atom("multiparam"));
2212
2213        let sig = get_signature_of_function_like_identifier(&id, &codebase);
2214        assert!(sig.is_some());
2215
2216        let sig = sig.unwrap();
2217        assert_eq!(sig.get_parameters().len(), 3);
2218
2219        let third_param = &sig.get_parameters()[2];
2220        assert!(third_param.has_default());
2221    }
2222
2223    #[test]
2224    fn test_expand_preserves_by_reference_flag() {
2225        let code = r"<?php class Foo {}";
2226        let codebase = create_test_codebase(code);
2227
2228        let mut input = make_self_object();
2229        input.flags.insert(UnionFlags::BY_REFERENCE);
2230
2231        let options = options_with_self("Foo");
2232        let mut actual = input.clone();
2233        expand_union(&codebase, &mut actual, &options);
2234
2235        assert!(actual.flags.contains(UnionFlags::BY_REFERENCE));
2236    }
2237
2238    #[test]
2239    fn test_expand_preserves_possibly_undefined_flag() {
2240        let code = r"<?php class Foo {}";
2241        let codebase = create_test_codebase(code);
2242
2243        let mut input = make_self_object();
2244        input.flags.insert(UnionFlags::POSSIBLY_UNDEFINED);
2245
2246        let options = options_with_self("Foo");
2247        let mut actual = input.clone();
2248        expand_union(&codebase, &mut actual, &options);
2249
2250        assert!(actual.flags.contains(UnionFlags::POSSIBLY_UNDEFINED));
2251    }
2252
2253    #[test]
2254    fn test_expand_multiple_self_in_union() {
2255        let code = r"<?php class Foo {}";
2256        let codebase = create_test_codebase(code);
2257
2258        let input = TUnion::from_vec(vec![
2259            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2260            TAtomic::Object(TObject::Named(TNamedObject::new(atom("self")))),
2261        ]);
2262
2263        let options = options_with_self("Foo");
2264        let mut actual = input.clone();
2265        expand_union(&codebase, &mut actual, &options);
2266
2267        assert!(actual.types.len() <= 2);
2268    }
2269
2270    #[test]
2271    fn test_expand_deeply_nested_types() {
2272        let code = r"<?php class Foo {}";
2273        let codebase = create_test_codebase(code);
2274
2275        let inner = TList::new(Box::new(make_self_object()));
2276        let middle = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(inner)))));
2277        let outer = TList::new(Box::new(TUnion::from_atomic(TAtomic::Array(TArray::List(middle)))));
2278        let input = TUnion::from_atomic(TAtomic::Array(TArray::List(outer)));
2279
2280        let options = options_with_self("Foo");
2281        let mut actual = input.clone();
2282        expand_union(&codebase, &mut actual, &options);
2283
2284        if let TAtomic::Array(TArray::List(outer)) = &actual.types[0]
2285            && let TAtomic::Array(TArray::List(middle)) = &outer.element_type.types[0]
2286            && let TAtomic::Array(TArray::List(inner)) = &middle.element_type.types[0]
2287        {
2288            assert!(inner.element_type.types.iter().any(|t| {
2289                if let TAtomic::Object(TObject::Named(named)) = t {
2290                    named.name == ascii_lowercase_atom("foo")
2291                } else {
2292                    false
2293                }
2294            }));
2295        }
2296    }
2297
2298    #[test]
2299    fn test_expand_with_all_options_disabled() {
2300        let code = r"<?php class Foo {}";
2301        let codebase = create_test_codebase(code);
2302
2303        let input = make_self_object();
2304        let options = TypeExpansionOptions {
2305            self_class: None,
2306            static_class_type: StaticClassType::None,
2307            parent_class: None,
2308            evaluate_class_constants: false,
2309            evaluate_conditional_types: false,
2310            function_is_final: false,
2311            expand_generic: false,
2312            expand_templates: false,
2313        };
2314
2315        let mut actual = input.clone();
2316        expand_union(&codebase, &mut actual, &options);
2317
2318        assert!(actual.types.iter().any(|t| {
2319            if let TAtomic::Object(TObject::Named(named)) = t { named.name == atom("self") } else { false }
2320        }));
2321    }
2322
2323    #[test]
2324    fn test_expand_already_expanded_type() {
2325        let code = r"<?php class Foo {}";
2326        let codebase = create_test_codebase(code);
2327
2328        let input = make_named_object("Foo");
2329        let options = options_with_self("Foo");
2330
2331        let mut actual = input.clone();
2332        expand_union(&codebase, &mut actual, &options);
2333
2334        let mut actual2 = actual.clone();
2335        expand_union(&codebase, &mut actual2, &options);
2336
2337        assert_eq!(actual.types.as_ref(), actual2.types.as_ref());
2338    }
2339
2340    #[test]
2341    fn test_expand_complex_generic_class() {
2342        let code = r"<?php
2343            /**
2344             * @template T
2345             * @template U
2346             */
2347            class Container {}
2348        ";
2349        let codebase = create_test_codebase(code);
2350
2351        let named = TNamedObject::new_with_type_parameters(
2352            ascii_lowercase_atom("container"),
2353            Some(vec![make_self_object(), make_static_object()]),
2354        );
2355        let input = TUnion::from_atomic(TAtomic::Object(TObject::Named(named)));
2356
2357        let options = TypeExpansionOptions {
2358            self_class: Some(ascii_lowercase_atom("foo")),
2359            static_class_type: StaticClassType::Name(ascii_lowercase_atom("bar")),
2360            ..Default::default()
2361        };
2362
2363        let mut actual = input.clone();
2364        expand_union(&codebase, &mut actual, &options);
2365
2366        if let TAtomic::Object(TObject::Named(named)) = &actual.types[0]
2367            && let Some(params) = &named.type_parameters
2368        {
2369            assert!(params[0].types.iter().any(|t| {
2370                if let TAtomic::Object(TObject::Named(named)) = t {
2371                    named.name == ascii_lowercase_atom("foo")
2372                } else {
2373                    false
2374                }
2375            }));
2376            assert!(params[1].types.iter().any(|t| {
2377                if let TAtomic::Object(TObject::Named(named)) = t {
2378                    named.name == ascii_lowercase_atom("bar")
2379                } else {
2380                    false
2381                }
2382            }));
2383        }
2384    }
2385}