Skip to main content

mago_codex/ttype/
union.rs

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