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