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