Skip to main content

mago_codex/ttype/
expander.rs

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