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