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