mago_codex/ttype/
union.rs

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