Skip to main content

mago_codex/ttype/
union.rs

1use std::borrow::Cow;
2use std::hash::Hash;
3use std::hash::Hasher;
4
5use bitflags::bitflags;
6use derivative::Derivative;
7use serde::Deserialize;
8use serde::Serialize;
9
10use mago_atom::Atom;
11use mago_atom::atom;
12use mago_atom::concat_atom;
13use mago_atom::empty_atom;
14
15use crate::metadata::CodebaseMetadata;
16use crate::reference::ReferenceSource;
17use crate::reference::SymbolReferences;
18use crate::symbol::Symbols;
19use crate::ttype::TType;
20use crate::ttype::TypeRef;
21use crate::ttype::atomic::TAtomic;
22use crate::ttype::atomic::array::TArray;
23use crate::ttype::atomic::array::key::ArrayKey;
24use crate::ttype::atomic::generic::TGenericParameter;
25use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
26use crate::ttype::atomic::object::TObject;
27use crate::ttype::atomic::object::named::TNamedObject;
28use crate::ttype::atomic::object::with_properties::TObjectWithProperties;
29use crate::ttype::atomic::populate_atomic_type;
30use crate::ttype::atomic::scalar::TScalar;
31use crate::ttype::atomic::scalar::bool::TBool;
32use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
33use crate::ttype::atomic::scalar::int::TInteger;
34use crate::ttype::atomic::scalar::string::TString;
35use crate::ttype::atomic::scalar::string::TStringLiteral;
36use crate::ttype::get_arraykey;
37use crate::ttype::get_int;
38use crate::ttype::get_mixed;
39
40bitflags! {
41    /// Flags representing various properties of a type union.
42    ///
43    /// This replaces 9 individual boolean fields with a compact 16-bit representation,
44    /// reducing memory usage from 9 bytes to 2 bytes per TUnion instance.
45    #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
46    pub struct UnionFlags: u16 {
47        /// Indicates the union had a template type at some point.
48        const HAD_TEMPLATE = 1 << 0;
49        /// Indicates the value is passed by reference.
50        const BY_REFERENCE = 1 << 1;
51        /// Indicates no references exist to this type.
52        const REFERENCE_FREE = 1 << 2;
53        /// Indicates the type may be undefined due to a try block.
54        const POSSIBLY_UNDEFINED_FROM_TRY = 1 << 3;
55        /// Indicates the type may be undefined.
56        const POSSIBLY_UNDEFINED = 1 << 4;
57        /// Indicates nullable issues should be ignored for this type.
58        const IGNORE_NULLABLE_ISSUES = 1 << 5;
59        /// Indicates falsable issues should be ignored for this type.
60        const IGNORE_FALSABLE_ISSUES = 1 << 6;
61        /// Indicates the type came from a template default value.
62        const FROM_TEMPLATE_DEFAULT = 1 << 7;
63        /// Indicates the type has been populated with codebase information.
64        const POPULATED = 1 << 8;
65        /// Indicates the null in this union came from nullsafe short-circuit.
66        const NULLSAFE_NULL = 1 << 9;
67    }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize, Eq, Derivative, PartialOrd, Ord)]
71pub struct TUnion {
72    pub types: Cow<'static, [TAtomic]>,
73    pub flags: UnionFlags,
74}
75
76impl Hash for TUnion {
77    fn hash<H: Hasher>(&self, state: &mut H) {
78        for t in self.types.as_ref() {
79            t.hash(state);
80        }
81    }
82}
83
84impl TUnion {
85    /// The primary constructor for creating a `TUnion` from a Cow.
86    ///
87    /// This is the most basic way to create a `TUnion` and is used by both the
88    /// zero-allocation static helpers and the `from_vec` constructor.
89    #[must_use]
90    pub fn new(types: Cow<'static, [TAtomic]>) -> TUnion {
91        TUnion { types, flags: UnionFlags::empty() }
92    }
93
94    /// Creates a `TUnion` from an owned Vec, performing necessary cleanup.
95    ///
96    /// This preserves the original logic for cleaning up dynamically created unions,
97    /// such as removing redundant `never` types.
98    ///
99    /// # Panics
100    ///
101    /// In debug builds, panics if:
102    /// - The input Vec is empty (unions must contain at least one type)
103    /// - The input contains a mix of `never` types with other types (invalid union construction)
104    #[must_use]
105    pub fn from_vec(mut types: Vec<TAtomic>) -> TUnion {
106        if cfg!(debug_assertions) {
107            assert!(
108                !types.is_empty(),
109                "TUnion::from_vec() received an empty Vec. This indicates a logic error \
110                 in type construction - unions must contain at least one type. \
111                 Consider using TAtomic::Never for empty/impossible types."
112            );
113
114            if types.len() > 1
115                && types.iter().any(|atomic| {
116                    atomic.is_never() || atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
117                })
118            {
119                panic!(
120                    "TUnion::from_vec() received a mix of 'never' and other types. \
121                     This indicates a logic error - 'never' should be filtered out before \
122                     creating unions since (A | never) = A. Types received: {types:#?}"
123                )
124            }
125        } else {
126            // If we have more than one type, 'never' is redundant and can be removed,
127            // as the union `A|never` is simply `A`.
128            if types.len() > 1 {
129                types.retain(|atomic| {
130                    !atomic.is_never() && !atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
131                });
132            }
133
134            // If the vector was originally empty, or contained only 'never' types
135            // which were removed, ensure the final union is `never`.
136            if types.is_empty() {
137                types.push(TAtomic::Never);
138            }
139        }
140
141        Self::new(Cow::Owned(types))
142    }
143
144    /// Creates a `TUnion` from a single atomic type, which can be either
145    /// borrowed from a static source or owned.
146    ///
147    /// This function is a key optimization point. When passed a `Cow::Borrowed`,
148    /// it creates the `TUnion` without any heap allocation.
149    #[must_use]
150    pub fn from_single(atomic: Cow<'static, TAtomic>) -> TUnion {
151        let types_cow = match atomic {
152            Cow::Borrowed(borrowed_atomic) => Cow::Borrowed(std::slice::from_ref(borrowed_atomic)),
153            Cow::Owned(owned_atomic) => Cow::Owned(vec![owned_atomic]),
154        };
155
156        TUnion::new(types_cow)
157    }
158
159    /// Creates a `TUnion` from a single owned atomic type.
160    #[must_use]
161    pub fn from_atomic(atomic: TAtomic) -> TUnion {
162        TUnion::new(Cow::Owned(vec![atomic]))
163    }
164
165    #[inline]
166    pub fn set_possibly_undefined(&mut self, possibly_undefined: bool, from_try: Option<bool>) {
167        let from_try = from_try.unwrap_or(self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY));
168
169        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED, possibly_undefined);
170        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, from_try);
171    }
172
173    #[inline]
174    #[must_use]
175    pub const fn had_template(&self) -> bool {
176        self.flags.contains(UnionFlags::HAD_TEMPLATE)
177    }
178
179    #[inline]
180    #[must_use]
181    pub const fn by_reference(&self) -> bool {
182        self.flags.contains(UnionFlags::BY_REFERENCE)
183    }
184
185    #[inline]
186    #[must_use]
187    pub const fn reference_free(&self) -> bool {
188        self.flags.contains(UnionFlags::REFERENCE_FREE)
189    }
190
191    #[inline]
192    #[must_use]
193    pub const fn possibly_undefined_from_try(&self) -> bool {
194        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
195    }
196
197    #[inline]
198    #[must_use]
199    pub const fn possibly_undefined(&self) -> bool {
200        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED)
201    }
202
203    #[inline]
204    #[must_use]
205    pub const fn ignore_nullable_issues(&self) -> bool {
206        self.flags.contains(UnionFlags::IGNORE_NULLABLE_ISSUES)
207    }
208
209    #[inline]
210    #[must_use]
211    pub const fn ignore_falsable_issues(&self) -> bool {
212        self.flags.contains(UnionFlags::IGNORE_FALSABLE_ISSUES)
213    }
214
215    #[inline]
216    #[must_use]
217    pub const fn from_template_default(&self) -> bool {
218        self.flags.contains(UnionFlags::FROM_TEMPLATE_DEFAULT)
219    }
220
221    #[inline]
222    #[must_use]
223    pub const fn populated(&self) -> bool {
224        self.flags.contains(UnionFlags::POPULATED)
225    }
226
227    #[inline]
228    #[must_use]
229    pub const fn has_nullsafe_null(&self) -> bool {
230        self.flags.contains(UnionFlags::NULLSAFE_NULL)
231    }
232
233    #[inline]
234    pub fn set_had_template(&mut self, value: bool) {
235        self.flags.set(UnionFlags::HAD_TEMPLATE, value);
236    }
237
238    #[inline]
239    pub fn set_by_reference(&mut self, value: bool) {
240        self.flags.set(UnionFlags::BY_REFERENCE, value);
241    }
242
243    #[inline]
244    pub fn set_reference_free(&mut self, value: bool) {
245        self.flags.set(UnionFlags::REFERENCE_FREE, value);
246    }
247
248    #[inline]
249    pub fn set_possibly_undefined_from_try(&mut self, value: bool) {
250        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, value);
251    }
252
253    #[inline]
254    pub fn set_ignore_nullable_issues(&mut self, value: bool) {
255        self.flags.set(UnionFlags::IGNORE_NULLABLE_ISSUES, value);
256    }
257
258    #[inline]
259    pub fn set_ignore_falsable_issues(&mut self, value: bool) {
260        self.flags.set(UnionFlags::IGNORE_FALSABLE_ISSUES, value);
261    }
262
263    #[inline]
264    pub fn set_from_template_default(&mut self, value: bool) {
265        self.flags.set(UnionFlags::FROM_TEMPLATE_DEFAULT, value);
266    }
267
268    #[inline]
269    pub fn set_populated(&mut self, value: bool) {
270        self.flags.set(UnionFlags::POPULATED, value);
271    }
272
273    #[inline]
274    pub fn set_nullsafe_null(&mut self, value: bool) {
275        self.flags.set(UnionFlags::NULLSAFE_NULL, value);
276    }
277
278    /// Creates a new `TUnion` with the same properties as the original, but with a new set of types.
279    #[must_use]
280    pub fn clone_with_types(&self, types: Vec<TAtomic>) -> TUnion {
281        TUnion { types: Cow::Owned(types), flags: self.flags }
282    }
283
284    #[must_use]
285    pub fn to_non_nullable(&self) -> TUnion {
286        TUnion { types: Cow::Owned(self.get_non_nullable_types()), flags: self.flags }
287    }
288
289    #[must_use]
290    pub fn to_truthy(&self) -> TUnion {
291        TUnion { types: Cow::Owned(self.get_truthy_types()), flags: self.flags }
292    }
293
294    #[must_use]
295    pub fn get_non_nullable_types(&self) -> Vec<TAtomic> {
296        self.types
297            .iter()
298            .filter_map(|t| match t {
299                TAtomic::Null | TAtomic::Void => None,
300                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
301                    parameter_name: parameter.parameter_name,
302                    defining_entity: parameter.defining_entity,
303                    intersection_types: parameter.intersection_types.clone(),
304                    constraint: Box::new(parameter.constraint.to_non_nullable()),
305                })),
306                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_is_non_null(true))),
307                atomic => Some(atomic.clone()),
308            })
309            .collect()
310    }
311
312    #[must_use]
313    pub fn get_truthy_types(&self) -> Vec<TAtomic> {
314        self.types
315            .iter()
316            .filter_map(|t| match t {
317                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
318                    parameter_name: parameter.parameter_name,
319                    defining_entity: parameter.defining_entity,
320                    intersection_types: parameter.intersection_types.clone(),
321                    constraint: Box::new(parameter.constraint.to_truthy()),
322                })),
323                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_truthiness(TMixedTruthiness::Truthy))),
324                atomic => {
325                    if atomic.is_falsy() {
326                        None
327                    } else {
328                        Some(atomic.clone())
329                    }
330                }
331            })
332            .collect()
333    }
334
335    /// Adds `null` to the union type, making it nullable.
336    #[must_use]
337    pub fn as_nullable(mut self) -> TUnion {
338        let types = self.types.to_mut();
339
340        for atomic in types.iter_mut() {
341            if let TAtomic::Mixed(mixed) = atomic {
342                *mixed = mixed.with_is_non_null(false);
343            }
344        }
345
346        if !types.iter().any(|atomic| atomic.is_null() || atomic.is_mixed()) {
347            types.push(TAtomic::Null);
348        }
349
350        self
351    }
352
353    /// Removes a specific atomic type from the union.
354    pub fn remove_type(&mut self, bad_type: &TAtomic) {
355        self.types.to_mut().retain(|t| t != bad_type);
356    }
357
358    /// Replaces a specific atomic type in the union with a new type.
359    pub fn replace_type(&mut self, remove_type: &TAtomic, add_type: TAtomic) {
360        let types = self.types.to_mut();
361
362        if let Some(index) = types.iter().position(|t| t == remove_type) {
363            types[index] = add_type;
364        } else {
365            types.push(add_type);
366        }
367    }
368
369    #[must_use]
370    pub fn is_int(&self) -> bool {
371        for atomic in self.types.as_ref() {
372            if !atomic.is_int() {
373                return false;
374            }
375        }
376
377        true
378    }
379
380    #[must_use]
381    pub fn has_int_or_float(&self) -> bool {
382        for atomic in self.types.as_ref() {
383            if atomic.is_int_or_float() {
384                return true;
385            }
386        }
387
388        false
389    }
390
391    #[must_use]
392    pub fn has_int_and_float(&self) -> bool {
393        let mut has_int = false;
394        let mut has_float = false;
395
396        for atomic in self.types.as_ref() {
397            if atomic.is_int() {
398                has_int = true;
399            } else if atomic.is_float() {
400                has_float = true;
401            } else if atomic.is_int_or_float() {
402                has_int = true;
403                has_float = true;
404            }
405
406            if has_int && has_float {
407                return true;
408            }
409        }
410
411        false
412    }
413
414    #[must_use]
415    pub fn has_int_and_string(&self) -> bool {
416        let mut has_int = false;
417        let mut has_string = false;
418
419        for atomic in self.types.as_ref() {
420            if atomic.is_int() {
421                has_int = true;
422            } else if atomic.is_string() {
423                has_string = true;
424            } else if atomic.is_array_key() {
425                has_int = true;
426                has_string = true;
427            }
428
429            if has_int && has_string {
430                return true;
431            }
432        }
433
434        false
435    }
436
437    #[must_use]
438    pub fn has_int(&self) -> bool {
439        for atomic in self.types.as_ref() {
440            if atomic.is_int() || atomic.is_array_key() || atomic.is_numeric() {
441                return true;
442            }
443        }
444
445        false
446    }
447
448    #[must_use]
449    pub fn has_float(&self) -> bool {
450        for atomic in self.types.as_ref() {
451            if atomic.is_float() {
452                return true;
453            }
454        }
455
456        false
457    }
458
459    #[must_use]
460    pub fn is_array_key(&self) -> bool {
461        for atomic in self.types.as_ref() {
462            if atomic.is_array_key() {
463                continue;
464            }
465
466            return false;
467        }
468
469        true
470    }
471
472    #[must_use]
473    pub fn is_any_string(&self) -> bool {
474        for atomic in self.types.as_ref() {
475            if !atomic.is_any_string() {
476                return false;
477            }
478        }
479
480        true
481    }
482
483    pub fn is_string(&self) -> bool {
484        self.types.iter().all(TAtomic::is_string) && !self.types.is_empty()
485    }
486
487    #[must_use]
488    pub fn is_always_array_key(&self, ignore_never: bool) -> bool {
489        self.types.iter().all(|atomic| match atomic {
490            TAtomic::Never => ignore_never,
491            TAtomic::Scalar(scalar) => matches!(
492                scalar,
493                TScalar::ArrayKey | TScalar::Integer(_) | TScalar::String(_) | TScalar::ClassLikeString(_)
494            ),
495            TAtomic::GenericParameter(generic_parameter) => {
496                generic_parameter.constraint.is_always_array_key(ignore_never)
497            }
498            _ => false,
499        })
500    }
501
502    pub fn is_non_empty_string(&self) -> bool {
503        self.types.iter().all(TAtomic::is_non_empty_string) && !self.types.is_empty()
504    }
505
506    pub fn is_empty_array(&self) -> bool {
507        self.types.iter().all(TAtomic::is_empty_array) && !self.types.is_empty()
508    }
509
510    pub fn has_string(&self) -> bool {
511        self.types.iter().any(TAtomic::is_string) && !self.types.is_empty()
512    }
513
514    pub fn is_float(&self) -> bool {
515        self.types.iter().all(TAtomic::is_float) && !self.types.is_empty()
516    }
517
518    pub fn is_bool(&self) -> bool {
519        self.types.iter().all(TAtomic::is_bool) && !self.types.is_empty()
520    }
521
522    pub fn is_never(&self) -> bool {
523        self.types.iter().all(TAtomic::is_never) || self.types.is_empty()
524    }
525
526    pub fn is_never_template(&self) -> bool {
527        self.types.iter().all(TAtomic::is_templated_as_never) && !self.types.is_empty()
528    }
529
530    #[must_use]
531    pub fn is_placeholder(&self) -> bool {
532        self.types.iter().all(|t| matches!(t, TAtomic::Placeholder)) && !self.types.is_empty()
533    }
534
535    pub fn is_true(&self) -> bool {
536        self.types.iter().all(TAtomic::is_true) && !self.types.is_empty()
537    }
538
539    pub fn is_false(&self) -> bool {
540        self.types.iter().all(TAtomic::is_false) && !self.types.is_empty()
541    }
542
543    #[must_use]
544    pub fn is_nonnull(&self) -> bool {
545        self.types.len() == 1 && matches!(self.types[0], TAtomic::Mixed(mixed) if mixed.is_non_null())
546    }
547
548    pub fn is_numeric(&self) -> bool {
549        self.types.iter().all(TAtomic::is_numeric) && !self.types.is_empty()
550    }
551
552    pub fn is_int_or_float(&self) -> bool {
553        self.types.iter().all(TAtomic::is_int_or_float) && !self.types.is_empty()
554    }
555
556    /// Returns `Some(true)` if all types are effectively int, `Some(false)` if all are effectively float,
557    /// or `None` if mixed or neither. Handles unions like `1|2` (all int) or `3.4|4.5` (all float).
558    #[must_use]
559    pub fn effective_int_or_float(&self) -> Option<bool> {
560        let mut result: Option<bool> = None;
561        for atomic in self.types.as_ref() {
562            match atomic.effective_int_or_float() {
563                Some(is_int) => {
564                    if let Some(prev) = result {
565                        if prev != is_int {
566                            return None;
567                        }
568                    } else {
569                        result = Some(is_int);
570                    }
571                }
572                None => return None,
573            }
574        }
575
576        result
577    }
578
579    #[must_use]
580    pub fn is_mixed(&self) -> bool {
581        self.types.iter().all(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
582    }
583
584    pub fn is_mixed_template(&self) -> bool {
585        self.types.iter().all(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
586    }
587
588    #[must_use]
589    pub fn has_mixed(&self) -> bool {
590        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
591    }
592
593    pub fn has_mixed_template(&self) -> bool {
594        self.types.iter().any(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
595    }
596
597    #[must_use]
598    pub fn has_nullable_mixed(&self) -> bool {
599        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(mixed) if !mixed.is_non_null())) && !self.types.is_empty()
600    }
601
602    #[must_use]
603    pub fn has_void(&self) -> bool {
604        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
605    }
606
607    #[must_use]
608    pub fn has_null(&self) -> bool {
609        self.types.iter().any(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
610    }
611
612    #[must_use]
613    pub fn has_nullish(&self) -> bool {
614        self.types.iter().any(|t| match t {
615            TAtomic::Null | TAtomic::Void => true,
616            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
617            TAtomic::GenericParameter(parameter) => parameter.constraint.has_nullish(),
618            _ => false,
619        }) && !self.types.is_empty()
620    }
621
622    #[must_use]
623    pub fn is_nullable_mixed(&self) -> bool {
624        if self.types.len() != 1 {
625            return false;
626        }
627
628        match &self.types[0] {
629            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
630            _ => false,
631        }
632    }
633
634    #[must_use]
635    pub fn is_falsy_mixed(&self) -> bool {
636        if self.types.len() != 1 {
637            return false;
638        }
639
640        matches!(&self.types[0], &TAtomic::Mixed(mixed) if mixed.is_falsy())
641    }
642
643    #[must_use]
644    pub fn is_vanilla_mixed(&self) -> bool {
645        if self.types.len() != 1 {
646            return false;
647        }
648
649        self.types[0].is_vanilla_mixed()
650    }
651
652    #[must_use]
653    pub fn is_templated_as_vanilla_mixed(&self) -> bool {
654        if self.types.len() != 1 {
655            return false;
656        }
657
658        self.types[0].is_templated_as_vanilla_mixed()
659    }
660
661    #[must_use]
662    pub fn has_template_or_static(&self) -> bool {
663        for atomic in self.types.as_ref() {
664            if let TAtomic::GenericParameter(_) = atomic {
665                return true;
666            }
667
668            if let TAtomic::Object(TObject::Named(named_object)) = atomic {
669                if named_object.is_this() {
670                    return true;
671                }
672
673                if let Some(intersections) = named_object.get_intersection_types() {
674                    for intersection in intersections {
675                        if let TAtomic::GenericParameter(_) = intersection {
676                            return true;
677                        }
678                    }
679                }
680            }
681        }
682
683        false
684    }
685
686    #[must_use]
687    pub fn has_template(&self) -> bool {
688        for atomic in self.types.as_ref() {
689            if let TAtomic::GenericParameter(_) = atomic {
690                return true;
691            }
692
693            if let Some(intersections) = atomic.get_intersection_types() {
694                for intersection in intersections {
695                    if let TAtomic::GenericParameter(_) = intersection {
696                        return true;
697                    }
698                }
699            }
700        }
701
702        false
703    }
704
705    #[must_use]
706    pub fn has_template_types(&self) -> bool {
707        let all_child_nodes = self.get_all_child_nodes();
708
709        for child_node in all_child_nodes {
710            if let TypeRef::Atomic(
711                TAtomic::GenericParameter(_)
712                | TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })),
713            ) = child_node
714            {
715                return true;
716            }
717        }
718
719        false
720    }
721
722    #[must_use]
723    pub fn get_template_types(&self) -> Vec<&TAtomic> {
724        let all_child_nodes = self.get_all_child_nodes();
725
726        let mut template_types = Vec::new();
727
728        for child_node in all_child_nodes {
729            if let TypeRef::Atomic(inner) = child_node {
730                match inner {
731                    TAtomic::GenericParameter(_) => {
732                        template_types.push(inner);
733                    }
734                    TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })) => {
735                        template_types.push(inner);
736                    }
737                    _ => {}
738                }
739            }
740        }
741
742        template_types
743    }
744
745    pub fn is_objecty(&self) -> bool {
746        for atomic in self.types.as_ref() {
747            if let &TAtomic::Object(_) = atomic {
748                continue;
749            }
750
751            if let TAtomic::Callable(callable) = atomic
752                && callable.get_signature().is_none_or(super::atomic::callable::TCallableSignature::is_closure)
753            {
754                continue;
755            }
756
757            return false;
758        }
759
760        true
761    }
762
763    #[must_use]
764    pub fn is_generator(&self) -> bool {
765        for atomic in self.types.as_ref() {
766            if atomic.is_generator() {
767                continue;
768            }
769
770            return false;
771        }
772
773        true
774    }
775
776    #[must_use]
777    pub fn extends_or_implements(&self, codebase: &CodebaseMetadata, interface: &str) -> bool {
778        for atomic in self.types.as_ref() {
779            if !atomic.extends_or_implements(codebase, interface) {
780                return false;
781            }
782        }
783
784        true
785    }
786
787    #[must_use]
788    pub fn is_generic_parameter(&self) -> bool {
789        self.types.len() == 1 && matches!(self.types[0], TAtomic::GenericParameter(_))
790    }
791
792    #[must_use]
793    pub fn get_generic_parameter_constraint(&self) -> Option<&TUnion> {
794        if self.is_generic_parameter()
795            && let TAtomic::GenericParameter(parameter) = &self.types[0]
796        {
797            return Some(&parameter.constraint);
798        }
799
800        None
801    }
802
803    #[must_use]
804    pub fn is_null(&self) -> bool {
805        self.types.iter().all(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
806    }
807
808    #[must_use]
809    pub fn is_nullable(&self) -> bool {
810        self.types.iter().any(|t| match t {
811            TAtomic::Null => self.types.len() >= 2,
812            TAtomic::GenericParameter(param) => param.constraint.is_nullable(),
813            _ => false,
814        })
815    }
816
817    #[must_use]
818    pub fn is_void(&self) -> bool {
819        self.types.iter().all(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
820    }
821
822    #[must_use]
823    pub fn is_voidable(&self) -> bool {
824        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
825    }
826
827    pub fn has_resource(&self) -> bool {
828        self.types.iter().any(TAtomic::is_resource)
829    }
830
831    pub fn is_resource(&self) -> bool {
832        self.types.iter().all(TAtomic::is_resource) && !self.types.is_empty()
833    }
834
835    pub fn is_array(&self) -> bool {
836        self.types.iter().all(TAtomic::is_array) && !self.types.is_empty()
837    }
838
839    pub fn is_list(&self) -> bool {
840        self.types.iter().all(TAtomic::is_list) && !self.types.is_empty()
841    }
842
843    pub fn is_vanilla_array(&self) -> bool {
844        self.types.iter().all(TAtomic::is_vanilla_array) && !self.types.is_empty()
845    }
846
847    pub fn is_keyed_array(&self) -> bool {
848        self.types.iter().all(TAtomic::is_keyed_array) && !self.types.is_empty()
849    }
850
851    pub fn is_falsable(&self) -> bool {
852        self.types.len() >= 2 && self.types.iter().any(TAtomic::is_false)
853    }
854
855    #[must_use]
856    pub fn has_bool(&self) -> bool {
857        self.types.iter().any(|t| t.is_bool() || t.is_generic_scalar()) && !self.types.is_empty()
858    }
859
860    /// Checks if the union explicitly contains the generic `scalar` type.
861    ///
862    /// This is a specific check for the `scalar` type itself, not for a
863    /// combination of types that would form a scalar (e.g., `int|string|bool|float`).
864    /// For that, see `has_scalar_combination`.
865    pub fn has_scalar(&self) -> bool {
866        self.types.iter().any(TAtomic::is_generic_scalar)
867    }
868
869    /// Checks if the union contains a combination of types that is equivalent
870    /// to the generic `scalar` type (i.e., contains `int`, `float`, `bool`, and `string`).
871    #[must_use]
872    pub fn has_scalar_combination(&self) -> bool {
873        const HAS_INT: u8 = 1 << 0;
874        const HAS_FLOAT: u8 = 1 << 1;
875        const HAS_BOOL: u8 = 1 << 2;
876        const HAS_STRING: u8 = 1 << 3;
877        const ALL_SCALARS: u8 = HAS_INT | HAS_FLOAT | HAS_BOOL | HAS_STRING;
878
879        let mut flags = 0u8;
880
881        for atomic in self.types.as_ref() {
882            if atomic.is_int() {
883                flags |= HAS_INT;
884            } else if atomic.is_float() {
885                flags |= HAS_FLOAT;
886            } else if atomic.is_bool() {
887                flags |= HAS_BOOL;
888            } else if atomic.is_string() {
889                flags |= HAS_STRING;
890            } else if atomic.is_array_key() {
891                flags |= HAS_INT | HAS_STRING;
892            } else if atomic.is_numeric() {
893                // We don't add `string` as `numeric-string` does not contain `string` type
894                flags |= HAS_INT | HAS_FLOAT;
895            } else if atomic.is_generic_scalar() {
896                return true;
897            }
898
899            // Early exit if we've already found all scalar types
900            if flags == ALL_SCALARS {
901                return true;
902            }
903        }
904
905        flags == ALL_SCALARS
906    }
907    pub fn has_array_key(&self) -> bool {
908        self.types.iter().any(TAtomic::is_array_key)
909    }
910
911    pub fn has_iterable(&self) -> bool {
912        self.types.iter().any(TAtomic::is_iterable) && !self.types.is_empty()
913    }
914
915    pub fn has_array(&self) -> bool {
916        self.types.iter().any(TAtomic::is_array) && !self.types.is_empty()
917    }
918
919    #[must_use]
920    pub fn has_traversable(&self, codebase: &CodebaseMetadata) -> bool {
921        self.types.iter().any(|atomic| atomic.is_traversable(codebase)) && !self.types.is_empty()
922    }
923
924    #[must_use]
925    pub fn has_array_key_like(&self) -> bool {
926        self.types.iter().any(|atomic| atomic.is_array_key() || atomic.is_int() || atomic.is_string())
927    }
928
929    pub fn has_numeric(&self) -> bool {
930        self.types.iter().any(TAtomic::is_numeric) && !self.types.is_empty()
931    }
932
933    pub fn is_always_truthy(&self) -> bool {
934        self.types.iter().all(TAtomic::is_truthy) && !self.types.is_empty()
935    }
936
937    pub fn is_always_falsy(&self) -> bool {
938        self.types.iter().all(TAtomic::is_falsy) && !self.types.is_empty()
939    }
940
941    #[must_use]
942    pub fn is_literal_of(&self, other: &TUnion) -> bool {
943        let Some(other_atomic_type) = other.types.first() else {
944            return false;
945        };
946
947        match other_atomic_type {
948            TAtomic::Scalar(TScalar::String(_)) => {
949                for self_atomic_type in self.types.as_ref() {
950                    if self_atomic_type.is_string_of_literal_origin() {
951                        continue;
952                    }
953
954                    return false;
955                }
956
957                true
958            }
959            TAtomic::Scalar(TScalar::Integer(_)) => {
960                for self_atomic_type in self.types.as_ref() {
961                    if self_atomic_type.is_literal_int() {
962                        continue;
963                    }
964
965                    return false;
966                }
967
968                true
969            }
970            TAtomic::Scalar(TScalar::Float(_)) => {
971                for self_atomic_type in self.types.as_ref() {
972                    if self_atomic_type.is_literal_float() {
973                        continue;
974                    }
975
976                    return false;
977                }
978
979                true
980            }
981            _ => false,
982        }
983    }
984
985    #[must_use]
986    pub fn all_literals(&self) -> bool {
987        self.types
988            .iter()
989            .all(|atomic| atomic.is_string_of_literal_origin() || atomic.is_literal_int() || atomic.is_literal_float())
990    }
991
992    #[must_use]
993    pub fn has_static_object(&self) -> bool {
994        self.types
995            .iter()
996            .any(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_this()))
997    }
998
999    #[must_use]
1000    pub fn is_static_object(&self) -> bool {
1001        self.types
1002            .iter()
1003            .all(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_this()))
1004    }
1005
1006    #[inline]
1007    #[must_use]
1008    pub fn is_single(&self) -> bool {
1009        self.types.len() == 1
1010    }
1011
1012    #[inline]
1013    #[must_use]
1014    pub fn get_single_string(&self) -> Option<&TString> {
1015        if self.is_single()
1016            && let TAtomic::Scalar(TScalar::String(string)) = &self.types[0]
1017        {
1018            Some(string)
1019        } else {
1020            None
1021        }
1022    }
1023
1024    #[inline]
1025    #[must_use]
1026    pub fn get_single_array(&self) -> Option<&TArray> {
1027        if self.is_single()
1028            && let TAtomic::Array(array) = &self.types[0]
1029        {
1030            Some(array)
1031        } else {
1032            None
1033        }
1034    }
1035
1036    #[inline]
1037    #[must_use]
1038    pub fn get_single_bool(&self) -> Option<&TBool> {
1039        if self.is_single()
1040            && let TAtomic::Scalar(TScalar::Bool(bool)) = &self.types[0]
1041        {
1042            Some(bool)
1043        } else {
1044            None
1045        }
1046    }
1047
1048    #[inline]
1049    #[must_use]
1050    pub fn get_single_named_object(&self) -> Option<&TNamedObject> {
1051        if self.is_single()
1052            && let TAtomic::Object(TObject::Named(named_object)) = &self.types[0]
1053        {
1054            Some(named_object)
1055        } else {
1056            None
1057        }
1058    }
1059
1060    #[inline]
1061    #[must_use]
1062    pub fn get_single_shaped_object(&self) -> Option<&TObjectWithProperties> {
1063        if self.is_single()
1064            && let TAtomic::Object(TObject::WithProperties(shaped_object)) = &self.types[0]
1065        {
1066            Some(shaped_object)
1067        } else {
1068            None
1069        }
1070    }
1071
1072    #[inline]
1073    #[must_use]
1074    pub fn get_single(&self) -> &TAtomic {
1075        &self.types[0]
1076    }
1077
1078    #[inline]
1079    #[must_use]
1080    pub fn get_single_owned(self) -> TAtomic {
1081        self.types[0].clone()
1082    }
1083
1084    #[inline]
1085    #[must_use]
1086    pub fn is_named_object(&self) -> bool {
1087        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1088    }
1089
1090    #[must_use]
1091    pub fn is_enum(&self) -> bool {
1092        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_))))
1093    }
1094
1095    #[must_use]
1096    pub fn is_enum_case(&self) -> bool {
1097        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1098    }
1099
1100    #[must_use]
1101    pub fn is_single_enum_case(&self) -> bool {
1102        self.is_single()
1103            && self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1104    }
1105
1106    #[inline]
1107    #[must_use]
1108    pub fn has_named_object(&self) -> bool {
1109        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1110    }
1111
1112    #[inline]
1113    #[must_use]
1114    pub fn has_object(&self) -> bool {
1115        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Any | TObject::WithProperties(_))))
1116    }
1117
1118    #[inline]
1119    #[must_use]
1120    pub fn has_callable(&self) -> bool {
1121        self.types.iter().any(|t| matches!(t, TAtomic::Callable(_)))
1122    }
1123
1124    #[inline]
1125    #[must_use]
1126    pub fn is_callable(&self) -> bool {
1127        self.types.iter().all(|t| matches!(t, TAtomic::Callable(_)))
1128    }
1129
1130    #[inline]
1131    #[must_use]
1132    pub fn has_object_type(&self) -> bool {
1133        self.types.iter().any(|t| matches!(t, TAtomic::Object(_)))
1134    }
1135
1136    /// Return a vector of pairs containing the enum name, and their case name
1137    /// if specified.
1138    #[must_use]
1139    pub fn get_enum_cases(&self) -> Vec<(Atom, Option<Atom>)> {
1140        self.types
1141            .iter()
1142            .filter_map(|t| match t {
1143                TAtomic::Object(TObject::Enum(enum_object)) => Some((enum_object.name, enum_object.case)),
1144                _ => None,
1145            })
1146            .collect()
1147    }
1148
1149    #[must_use]
1150    pub fn get_single_int(&self) -> Option<TInteger> {
1151        if self.is_single() { self.get_single().get_integer() } else { None }
1152    }
1153
1154    #[must_use]
1155    pub fn get_single_literal_int_value(&self) -> Option<i64> {
1156        if self.is_single() { self.get_single().get_literal_int_value() } else { None }
1157    }
1158
1159    #[must_use]
1160    pub fn get_single_maximum_int_value(&self) -> Option<i64> {
1161        if self.is_single() { self.get_single().get_maximum_int_value() } else { None }
1162    }
1163
1164    #[must_use]
1165    pub fn get_single_minimum_int_value(&self) -> Option<i64> {
1166        if self.is_single() { self.get_single().get_minimum_int_value() } else { None }
1167    }
1168
1169    #[must_use]
1170    pub fn get_single_literal_float_value(&self) -> Option<f64> {
1171        if self.is_single() { self.get_single().get_literal_float_value() } else { None }
1172    }
1173
1174    #[must_use]
1175    pub fn get_single_literal_string_value(&self) -> Option<&str> {
1176        if self.is_single() { self.get_single().get_literal_string_value() } else { None }
1177    }
1178
1179    #[must_use]
1180    pub fn get_single_class_string_value(&self) -> Option<Atom> {
1181        if self.is_single() { self.get_single().get_class_string_value() } else { None }
1182    }
1183
1184    #[must_use]
1185    pub fn get_single_array_key(&self) -> Option<ArrayKey> {
1186        if self.is_single() { self.get_single().to_array_key() } else { None }
1187    }
1188
1189    #[must_use]
1190    pub fn get_single_key_of_array_like(&self) -> Option<TUnion> {
1191        if !self.is_single() {
1192            return None;
1193        }
1194
1195        match self.get_single() {
1196            TAtomic::Array(array) => match array {
1197                TArray::List(_) => Some(get_int()),
1198                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1199                    Some((k, _)) => Some(*k.clone()),
1200                    None => Some(get_arraykey()),
1201                },
1202            },
1203            _ => None,
1204        }
1205    }
1206
1207    #[must_use]
1208    pub fn get_single_value_of_array_like(&self) -> Option<Cow<'_, TUnion>> {
1209        if !self.is_single() {
1210            return None;
1211        }
1212
1213        match self.get_single() {
1214            TAtomic::Array(array) => match array {
1215                TArray::List(list) => Some(Cow::Borrowed(&list.element_type)),
1216                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1217                    Some((_, v)) => Some(Cow::Borrowed(v)),
1218                    None => Some(Cow::Owned(get_mixed())),
1219                },
1220            },
1221            _ => None,
1222        }
1223    }
1224
1225    #[must_use]
1226    pub fn get_literal_ints(&self) -> Vec<&TAtomic> {
1227        self.types.iter().filter(|a| a.is_literal_int()).collect()
1228    }
1229
1230    #[must_use]
1231    pub fn get_literal_strings(&self) -> Vec<&TAtomic> {
1232        self.types.iter().filter(|a| a.is_known_literal_string()).collect()
1233    }
1234
1235    #[must_use]
1236    pub fn get_literal_string_values(&self) -> Vec<Option<Atom>> {
1237        self.get_literal_strings()
1238            .into_iter()
1239            .map(|atom| match atom {
1240                TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(value)), .. })) => {
1241                    Some(*value)
1242                }
1243                _ => None,
1244            })
1245            .collect()
1246    }
1247
1248    #[must_use]
1249    pub fn has_literal_float(&self) -> bool {
1250        self.types.iter().any(|atomic| match atomic {
1251            TAtomic::Scalar(scalar) => scalar.is_literal_float(),
1252            _ => false,
1253        })
1254    }
1255
1256    #[must_use]
1257    pub fn has_literal_int(&self) -> bool {
1258        self.types.iter().any(|atomic| match atomic {
1259            TAtomic::Scalar(scalar) => scalar.is_literal_int(),
1260            _ => false,
1261        })
1262    }
1263
1264    #[must_use]
1265    pub fn has_literal_string(&self) -> bool {
1266        self.types.iter().any(|atomic| match atomic {
1267            TAtomic::Scalar(scalar) => scalar.is_known_literal_string(),
1268            _ => false,
1269        })
1270    }
1271
1272    #[must_use]
1273    pub fn has_literal_value(&self) -> bool {
1274        self.types.iter().any(|atomic| match atomic {
1275            TAtomic::Scalar(scalar) => scalar.is_literal_value(),
1276            _ => false,
1277        })
1278    }
1279
1280    #[must_use]
1281    pub fn accepts_false(&self) -> bool {
1282        self.types.iter().any(|t| match t {
1283            TAtomic::GenericParameter(parameter) => parameter.constraint.accepts_false(),
1284            TAtomic::Mixed(mixed) if !mixed.is_truthy() => true,
1285            TAtomic::Scalar(TScalar::Generic | TScalar::Bool(TBool { value: None | Some(false) })) => true,
1286            _ => false,
1287        })
1288    }
1289
1290    #[must_use]
1291    pub fn accepts_null(&self) -> bool {
1292        self.types.iter().any(|t| match t {
1293            TAtomic::GenericParameter(generic_parameter) => generic_parameter.constraint.accepts_null(),
1294            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
1295            TAtomic::Null => true,
1296            _ => false,
1297        })
1298    }
1299}
1300
1301impl TType for TUnion {
1302    fn get_child_nodes(&self) -> Vec<TypeRef<'_>> {
1303        self.types.iter().map(TypeRef::Atomic).collect()
1304    }
1305
1306    fn needs_population(&self) -> bool {
1307        !self.flags.contains(UnionFlags::POPULATED) && self.types.iter().any(super::TType::needs_population)
1308    }
1309
1310    fn is_expandable(&self) -> bool {
1311        if self.types.is_empty() {
1312            return true;
1313        }
1314
1315        self.types.iter().any(super::TType::is_expandable)
1316    }
1317
1318    fn is_complex(&self) -> bool {
1319        self.types.len() > 3 || self.types.iter().any(super::TType::is_complex)
1320    }
1321
1322    fn get_id(&self) -> Atom {
1323        let len = self.types.len();
1324
1325        let mut atomic_ids: Vec<Atom> = self
1326            .types
1327            .as_ref()
1328            .iter()
1329            .map(|atomic| {
1330                let id = atomic.get_id();
1331                if atomic.is_generic_parameter() || atomic.has_intersection_types() && len > 1 {
1332                    concat_atom!("(", id.as_str(), ")")
1333                } else {
1334                    id
1335                }
1336            })
1337            .collect();
1338
1339        if len <= 1 {
1340            return atomic_ids.pop().unwrap_or_else(empty_atom);
1341        }
1342
1343        atomic_ids.sort_unstable();
1344        let mut result = atomic_ids[0];
1345        for id in &atomic_ids[1..] {
1346            result = concat_atom!(result.as_str(), "|", id.as_str());
1347        }
1348
1349        result
1350    }
1351
1352    fn get_pretty_id_with_indent(&self, indent: usize) -> Atom {
1353        let len = self.types.len();
1354
1355        if len <= 1 {
1356            return self.types.first().map_or_else(empty_atom, |atomic| atomic.get_pretty_id_with_indent(indent));
1357        }
1358
1359        // Use multiline format for unions with more than 3 types
1360        if len > 3 {
1361            let mut atomic_ids: Vec<Atom> = self
1362                .types
1363                .as_ref()
1364                .iter()
1365                .map(|atomic| {
1366                    let id = atomic.get_pretty_id_with_indent(indent + 2);
1367                    if atomic.has_intersection_types() { concat_atom!("(", id.as_str(), ")") } else { id }
1368                })
1369                .collect();
1370
1371            atomic_ids.sort_unstable();
1372
1373            let mut result = String::new();
1374            result += &atomic_ids[0];
1375            for id in &atomic_ids[1..] {
1376                result += "\n";
1377                result += &" ".repeat(indent);
1378                result += "| ";
1379                result += id.as_str();
1380            }
1381
1382            atom(&result)
1383        } else {
1384            // Use inline format for smaller unions
1385            let mut atomic_ids: Vec<Atom> = self
1386                .types
1387                .as_ref()
1388                .iter()
1389                .map(|atomic| {
1390                    let id = atomic.get_pretty_id_with_indent(indent);
1391                    if atomic.has_intersection_types() && len > 1 { concat_atom!("(", id.as_str(), ")") } else { id }
1392                })
1393                .collect();
1394
1395            atomic_ids.sort_unstable();
1396            let mut result = atomic_ids[0];
1397            for id in &atomic_ids[1..] {
1398                result = concat_atom!(result.as_str(), " | ", id.as_str());
1399            }
1400
1401            result
1402        }
1403    }
1404}
1405
1406impl PartialEq for TUnion {
1407    fn eq(&self, other: &TUnion) -> bool {
1408        const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
1409            .union(UnionFlags::BY_REFERENCE)
1410            .union(UnionFlags::REFERENCE_FREE)
1411            .union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
1412            .union(UnionFlags::POSSIBLY_UNDEFINED)
1413            .union(UnionFlags::IGNORE_NULLABLE_ISSUES)
1414            .union(UnionFlags::IGNORE_FALSABLE_ISSUES)
1415            .union(UnionFlags::FROM_TEMPLATE_DEFAULT);
1416
1417        if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
1418            return false;
1419        }
1420
1421        let len = self.types.len();
1422        if len != other.types.len() {
1423            return false;
1424        }
1425
1426        for i in 0..len {
1427            let mut has_match = false;
1428            for j in 0..len {
1429                if self.types[i] == other.types[j] {
1430                    has_match = true;
1431                    break;
1432                }
1433            }
1434
1435            if !has_match {
1436                return false;
1437            }
1438        }
1439
1440        true
1441    }
1442}
1443
1444pub fn populate_union_type(
1445    unpopulated_union: &mut TUnion,
1446    codebase_symbols: &Symbols,
1447    reference_source: Option<&ReferenceSource>,
1448    symbol_references: &mut SymbolReferences,
1449    force: bool,
1450) {
1451    if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
1452        return;
1453    }
1454
1455    if !unpopulated_union.needs_population() {
1456        return;
1457    }
1458
1459    unpopulated_union.flags.insert(UnionFlags::POPULATED);
1460    let unpopulated_atomics = unpopulated_union.types.to_mut();
1461    for unpopulated_atomic in unpopulated_atomics {
1462        match unpopulated_atomic {
1463            TAtomic::Scalar(TScalar::ClassLikeString(
1464                TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
1465            )) => {
1466                let mut new_constraint = (**constraint).clone();
1467
1468                populate_atomic_type(&mut new_constraint, codebase_symbols, reference_source, symbol_references, force);
1469
1470                **constraint = new_constraint;
1471            }
1472            _ => {
1473                populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
1474            }
1475        }
1476    }
1477}