Skip to main content

mago_codex/ttype/
expander.rs

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