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