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