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