Skip to main content

mago_codex/ttype/
expander.rs

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