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