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 serde::Deserialize;
8use serde::Serialize;
9
10use mago_atom::Atom;
11use mago_atom::atom;
12use mago_atom::concat_atom;
13use mago_atom::empty_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    #[must_use]
88    pub fn new(types: Cow<'static, [TAtomic]>) -> TUnion {
89        TUnion { types, flags: UnionFlags::empty() }
90    }
91
92    /// Creates a `TUnion` from an owned Vec, performing necessary cleanup.
93    ///
94    /// This preserves the original logic for cleaning up dynamically created unions,
95    /// such as removing redundant `never` types.
96    ///
97    /// # Panics
98    ///
99    /// In debug builds, panics if:
100    /// - The input Vec is empty (unions must contain at least one type)
101    /// - The input contains a mix of `never` types with other types (invalid union construction)
102    #[must_use]
103    pub fn from_vec(mut types: Vec<TAtomic>) -> TUnion {
104        if cfg!(debug_assertions) {
105            assert!(
106                !types.is_empty(),
107                "TUnion::from_vec() received an empty Vec. This indicates a logic error \
108                 in type construction - unions must contain at least one type. \
109                 Consider using TAtomic::Never for empty/impossible types."
110            );
111
112            if types.len() > 1
113                && types.iter().any(|atomic| {
114                    atomic.is_never() || atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
115                })
116            {
117                panic!(
118                    "TUnion::from_vec() received a mix of 'never' and other types. \
119                     This indicates a logic error - 'never' should be filtered out before \
120                     creating unions since (A | never) = A. Types received: {types:#?}"
121                )
122            }
123        } else {
124            // If we have more than one type, 'never' is redundant and can be removed,
125            // as the union `A|never` is simply `A`.
126            if types.len() > 1 {
127                types.retain(|atomic| {
128                    !atomic.is_never() && !atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
129                });
130            }
131
132            // If the vector was originally empty, or contained only 'never' types
133            // which were removed, ensure the final union is `never`.
134            if types.is_empty() {
135                types.push(TAtomic::Never);
136            }
137        }
138
139        Self::new(Cow::Owned(types))
140    }
141
142    /// Creates a `TUnion` from a single atomic type, which can be either
143    /// borrowed from a static source or owned.
144    ///
145    /// This function is a key optimization point. When passed a `Cow::Borrowed`,
146    /// it creates the `TUnion` without any heap allocation.
147    #[must_use]
148    pub fn from_single(atomic: Cow<'static, TAtomic>) -> TUnion {
149        let types_cow = match atomic {
150            Cow::Borrowed(borrowed_atomic) => Cow::Borrowed(std::slice::from_ref(borrowed_atomic)),
151            Cow::Owned(owned_atomic) => Cow::Owned(vec![owned_atomic]),
152        };
153
154        TUnion::new(types_cow)
155    }
156
157    /// Creates a `TUnion` from a single owned atomic type.
158    #[must_use]
159    pub fn from_atomic(atomic: TAtomic) -> TUnion {
160        TUnion::new(Cow::Owned(vec![atomic]))
161    }
162
163    #[inline]
164    pub fn set_possibly_undefined(&mut self, possibly_undefined: bool, from_try: Option<bool>) {
165        let from_try = from_try.unwrap_or(self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY));
166
167        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED, possibly_undefined);
168        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, from_try);
169    }
170
171    #[inline]
172    #[must_use]
173    pub const fn had_template(&self) -> bool {
174        self.flags.contains(UnionFlags::HAD_TEMPLATE)
175    }
176
177    #[inline]
178    #[must_use]
179    pub const fn by_reference(&self) -> bool {
180        self.flags.contains(UnionFlags::BY_REFERENCE)
181    }
182
183    #[inline]
184    #[must_use]
185    pub const fn reference_free(&self) -> bool {
186        self.flags.contains(UnionFlags::REFERENCE_FREE)
187    }
188
189    #[inline]
190    #[must_use]
191    pub const fn possibly_undefined_from_try(&self) -> bool {
192        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
193    }
194
195    #[inline]
196    #[must_use]
197    pub const fn possibly_undefined(&self) -> bool {
198        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED)
199    }
200
201    #[inline]
202    #[must_use]
203    pub const fn ignore_nullable_issues(&self) -> bool {
204        self.flags.contains(UnionFlags::IGNORE_NULLABLE_ISSUES)
205    }
206
207    #[inline]
208    #[must_use]
209    pub const fn ignore_falsable_issues(&self) -> bool {
210        self.flags.contains(UnionFlags::IGNORE_FALSABLE_ISSUES)
211    }
212
213    #[inline]
214    #[must_use]
215    pub const fn from_template_default(&self) -> bool {
216        self.flags.contains(UnionFlags::FROM_TEMPLATE_DEFAULT)
217    }
218
219    #[inline]
220    #[must_use]
221    pub const fn populated(&self) -> bool {
222        self.flags.contains(UnionFlags::POPULATED)
223    }
224
225    #[inline]
226    pub fn set_had_template(&mut self, value: bool) {
227        self.flags.set(UnionFlags::HAD_TEMPLATE, value);
228    }
229
230    #[inline]
231    pub fn set_by_reference(&mut self, value: bool) {
232        self.flags.set(UnionFlags::BY_REFERENCE, value);
233    }
234
235    #[inline]
236    pub fn set_reference_free(&mut self, value: bool) {
237        self.flags.set(UnionFlags::REFERENCE_FREE, value);
238    }
239
240    #[inline]
241    pub fn set_possibly_undefined_from_try(&mut self, value: bool) {
242        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, value);
243    }
244
245    #[inline]
246    pub fn set_ignore_nullable_issues(&mut self, value: bool) {
247        self.flags.set(UnionFlags::IGNORE_NULLABLE_ISSUES, value);
248    }
249
250    #[inline]
251    pub fn set_ignore_falsable_issues(&mut self, value: bool) {
252        self.flags.set(UnionFlags::IGNORE_FALSABLE_ISSUES, value);
253    }
254
255    #[inline]
256    pub fn set_from_template_default(&mut self, value: bool) {
257        self.flags.set(UnionFlags::FROM_TEMPLATE_DEFAULT, value);
258    }
259
260    #[inline]
261    pub fn set_populated(&mut self, value: bool) {
262        self.flags.set(UnionFlags::POPULATED, value);
263    }
264
265    /// Creates a new `TUnion` with the same properties as the original, but with a new set of types.
266    #[must_use]
267    pub fn clone_with_types(&self, types: Vec<TAtomic>) -> TUnion {
268        TUnion { types: Cow::Owned(types), flags: self.flags }
269    }
270
271    #[must_use]
272    pub fn to_non_nullable(&self) -> TUnion {
273        TUnion { types: Cow::Owned(self.get_non_nullable_types()), flags: self.flags }
274    }
275
276    #[must_use]
277    pub fn to_truthy(&self) -> TUnion {
278        TUnion { types: Cow::Owned(self.get_truthy_types()), flags: self.flags }
279    }
280
281    #[must_use]
282    pub fn get_non_nullable_types(&self) -> Vec<TAtomic> {
283        self.types
284            .iter()
285            .filter_map(|t| match t {
286                TAtomic::Null | TAtomic::Void => None,
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: Box::new(parameter.constraint.to_non_nullable()),
292                })),
293                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_is_non_null(true))),
294                atomic => Some(atomic.clone()),
295            })
296            .collect()
297    }
298
299    #[must_use]
300    pub fn get_truthy_types(&self) -> Vec<TAtomic> {
301        self.types
302            .iter()
303            .filter_map(|t| match t {
304                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
305                    parameter_name: parameter.parameter_name,
306                    defining_entity: parameter.defining_entity,
307                    intersection_types: parameter.intersection_types.clone(),
308                    constraint: Box::new(parameter.constraint.to_truthy()),
309                })),
310                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_truthiness(TMixedTruthiness::Truthy))),
311                atomic => {
312                    if atomic.is_falsy() {
313                        None
314                    } else {
315                        Some(atomic.clone())
316                    }
317                }
318            })
319            .collect()
320    }
321
322    /// Adds `null` to the union type, making it nullable.
323    #[must_use]
324    pub fn as_nullable(mut self) -> TUnion {
325        let types = self.types.to_mut();
326
327        for atomic in types.iter_mut() {
328            if let TAtomic::Mixed(mixed) = atomic {
329                *mixed = mixed.with_is_non_null(false);
330            }
331        }
332
333        if !types.iter().any(|atomic| atomic.is_null() || atomic.is_mixed()) {
334            types.push(TAtomic::Null);
335        }
336
337        self
338    }
339
340    /// Removes a specific atomic type from the union.
341    pub fn remove_type(&mut self, bad_type: &TAtomic) {
342        self.types.to_mut().retain(|t| t != bad_type);
343    }
344
345    /// Replaces a specific atomic type in the union with a new type.
346    pub fn replace_type(&mut self, remove_type: &TAtomic, add_type: TAtomic) {
347        let types = self.types.to_mut();
348
349        if let Some(index) = types.iter().position(|t| t == remove_type) {
350            types[index] = add_type;
351        } else {
352            types.push(add_type);
353        }
354    }
355
356    #[must_use]
357    pub fn is_int(&self) -> bool {
358        for atomic in self.types.as_ref() {
359            if !atomic.is_int() {
360                return false;
361            }
362        }
363
364        true
365    }
366
367    #[must_use]
368    pub fn has_int_or_float(&self) -> bool {
369        for atomic in self.types.as_ref() {
370            if atomic.is_int_or_float() {
371                return true;
372            }
373        }
374
375        false
376    }
377
378    #[must_use]
379    pub fn has_int_and_float(&self) -> bool {
380        let mut has_int = false;
381        let mut has_float = false;
382
383        for atomic in self.types.as_ref() {
384            if atomic.is_int() {
385                has_int = true;
386            } else if atomic.is_float() {
387                has_float = true;
388            } else if atomic.is_int_or_float() {
389                has_int = true;
390                has_float = true;
391            }
392
393            if has_int && has_float {
394                return true;
395            }
396        }
397
398        false
399    }
400
401    #[must_use]
402    pub fn has_int_and_string(&self) -> bool {
403        let mut has_int = false;
404        let mut has_string = false;
405
406        for atomic in self.types.as_ref() {
407            if atomic.is_int() {
408                has_int = true;
409            } else if atomic.is_string() {
410                has_string = true;
411            } else if atomic.is_array_key() {
412                has_int = true;
413                has_string = true;
414            }
415
416            if has_int && has_string {
417                return true;
418            }
419        }
420
421        false
422    }
423
424    #[must_use]
425    pub fn has_int(&self) -> bool {
426        for atomic in self.types.as_ref() {
427            if atomic.is_int() || atomic.is_array_key() || atomic.is_numeric() {
428                return true;
429            }
430        }
431
432        false
433    }
434
435    #[must_use]
436    pub fn has_float(&self) -> bool {
437        for atomic in self.types.as_ref() {
438            if atomic.is_float() {
439                return true;
440            }
441        }
442
443        false
444    }
445
446    #[must_use]
447    pub fn is_array_key(&self) -> bool {
448        for atomic in self.types.as_ref() {
449            if atomic.is_array_key() {
450                continue;
451            }
452
453            return false;
454        }
455
456        true
457    }
458
459    #[must_use]
460    pub fn is_any_string(&self) -> bool {
461        for atomic in self.types.as_ref() {
462            if !atomic.is_any_string() {
463                return false;
464            }
465        }
466
467        true
468    }
469
470    pub fn is_string(&self) -> bool {
471        self.types.iter().all(TAtomic::is_string) && !self.types.is_empty()
472    }
473
474    #[must_use]
475    pub fn is_always_array_key(&self, ignore_never: bool) -> bool {
476        self.types.iter().all(|atomic| match atomic {
477            TAtomic::Never => ignore_never,
478            TAtomic::Scalar(scalar) => matches!(
479                scalar,
480                TScalar::ArrayKey | TScalar::Integer(_) | TScalar::String(_) | TScalar::ClassLikeString(_)
481            ),
482            TAtomic::GenericParameter(generic_parameter) => {
483                generic_parameter.constraint.is_always_array_key(ignore_never)
484            }
485            _ => false,
486        })
487    }
488
489    pub fn is_non_empty_string(&self) -> bool {
490        self.types.iter().all(TAtomic::is_non_empty_string) && !self.types.is_empty()
491    }
492
493    pub fn is_empty_array(&self) -> bool {
494        self.types.iter().all(TAtomic::is_empty_array) && !self.types.is_empty()
495    }
496
497    pub fn has_string(&self) -> bool {
498        self.types.iter().any(TAtomic::is_string) && !self.types.is_empty()
499    }
500
501    pub fn is_float(&self) -> bool {
502        self.types.iter().all(TAtomic::is_float) && !self.types.is_empty()
503    }
504
505    pub fn is_bool(&self) -> bool {
506        self.types.iter().all(TAtomic::is_bool) && !self.types.is_empty()
507    }
508
509    pub fn is_never(&self) -> bool {
510        self.types.iter().all(TAtomic::is_never) || self.types.is_empty()
511    }
512
513    pub fn is_never_template(&self) -> bool {
514        self.types.iter().all(TAtomic::is_templated_as_never) && !self.types.is_empty()
515    }
516
517    #[must_use]
518    pub fn is_placeholder(&self) -> bool {
519        self.types.iter().all(|t| matches!(t, TAtomic::Placeholder)) && !self.types.is_empty()
520    }
521
522    pub fn is_true(&self) -> bool {
523        self.types.iter().all(TAtomic::is_true) && !self.types.is_empty()
524    }
525
526    pub fn is_false(&self) -> bool {
527        self.types.iter().all(TAtomic::is_false) && !self.types.is_empty()
528    }
529
530    #[must_use]
531    pub fn is_nonnull(&self) -> bool {
532        self.types.len() == 1 && matches!(self.types[0], TAtomic::Mixed(mixed) if mixed.is_non_null())
533    }
534
535    pub fn is_numeric(&self) -> bool {
536        self.types.iter().all(TAtomic::is_numeric) && !self.types.is_empty()
537    }
538
539    pub fn is_int_or_float(&self) -> bool {
540        self.types.iter().all(TAtomic::is_int_or_float) && !self.types.is_empty()
541    }
542
543    /// Returns `Some(true)` if all types are effectively int, `Some(false)` if all are effectively float,
544    /// or `None` if mixed or neither. Handles unions like `1|2` (all int) or `3.4|4.5` (all float).
545    #[must_use]
546    pub fn effective_int_or_float(&self) -> Option<bool> {
547        let mut result: Option<bool> = None;
548        for atomic in self.types.as_ref() {
549            match atomic.effective_int_or_float() {
550                Some(is_int) => {
551                    if let Some(prev) = result {
552                        if prev != is_int {
553                            return None;
554                        }
555                    } else {
556                        result = Some(is_int);
557                    }
558                }
559                None => return None,
560            }
561        }
562
563        result
564    }
565
566    #[must_use]
567    pub fn is_mixed(&self) -> bool {
568        self.types.iter().all(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
569    }
570
571    pub fn is_mixed_template(&self) -> bool {
572        self.types.iter().all(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
573    }
574
575    #[must_use]
576    pub fn has_mixed(&self) -> bool {
577        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
578    }
579
580    pub fn has_mixed_template(&self) -> bool {
581        self.types.iter().any(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
582    }
583
584    #[must_use]
585    pub fn has_nullable_mixed(&self) -> bool {
586        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(mixed) if !mixed.is_non_null())) && !self.types.is_empty()
587    }
588
589    #[must_use]
590    pub fn has_void(&self) -> bool {
591        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
592    }
593
594    #[must_use]
595    pub fn has_null(&self) -> bool {
596        self.types.iter().any(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
597    }
598
599    #[must_use]
600    pub fn has_nullish(&self) -> bool {
601        self.types.iter().any(|t| match t {
602            TAtomic::Null | TAtomic::Void => true,
603            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
604            TAtomic::GenericParameter(parameter) => parameter.constraint.has_nullish(),
605            _ => false,
606        }) && !self.types.is_empty()
607    }
608
609    #[must_use]
610    pub fn is_nullable_mixed(&self) -> bool {
611        if self.types.len() != 1 {
612            return false;
613        }
614
615        match &self.types[0] {
616            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
617            _ => false,
618        }
619    }
620
621    #[must_use]
622    pub fn is_falsy_mixed(&self) -> bool {
623        if self.types.len() != 1 {
624            return false;
625        }
626
627        matches!(&self.types[0], &TAtomic::Mixed(mixed) if mixed.is_falsy())
628    }
629
630    #[must_use]
631    pub fn is_vanilla_mixed(&self) -> bool {
632        if self.types.len() != 1 {
633            return false;
634        }
635
636        self.types[0].is_vanilla_mixed()
637    }
638
639    #[must_use]
640    pub fn is_templated_as_vanilla_mixed(&self) -> bool {
641        if self.types.len() != 1 {
642            return false;
643        }
644
645        self.types[0].is_templated_as_vanilla_mixed()
646    }
647
648    #[must_use]
649    pub fn has_template_or_static(&self) -> bool {
650        for atomic in self.types.as_ref() {
651            if let TAtomic::GenericParameter(_) = atomic {
652                return true;
653            }
654
655            if let TAtomic::Object(TObject::Named(named_object)) = atomic {
656                if named_object.is_this() {
657                    return true;
658                }
659
660                if let Some(intersections) = named_object.get_intersection_types() {
661                    for intersection in intersections {
662                        if let TAtomic::GenericParameter(_) = intersection {
663                            return true;
664                        }
665                    }
666                }
667            }
668        }
669
670        false
671    }
672
673    #[must_use]
674    pub fn has_template(&self) -> bool {
675        for atomic in self.types.as_ref() {
676            if let TAtomic::GenericParameter(_) = atomic {
677                return true;
678            }
679
680            if let Some(intersections) = atomic.get_intersection_types() {
681                for intersection in intersections {
682                    if let TAtomic::GenericParameter(_) = intersection {
683                        return true;
684                    }
685                }
686            }
687        }
688
689        false
690    }
691
692    #[must_use]
693    pub fn has_template_types(&self) -> bool {
694        let all_child_nodes = self.get_all_child_nodes();
695
696        for child_node in all_child_nodes {
697            if let TypeRef::Atomic(
698                TAtomic::GenericParameter(_)
699                | TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })),
700            ) = child_node
701            {
702                return true;
703            }
704        }
705
706        false
707    }
708
709    #[must_use]
710    pub fn get_template_types(&self) -> Vec<&TAtomic> {
711        let all_child_nodes = self.get_all_child_nodes();
712
713        let mut template_types = Vec::new();
714
715        for child_node in all_child_nodes {
716            if let TypeRef::Atomic(inner) = child_node {
717                match inner {
718                    TAtomic::GenericParameter(_) => {
719                        template_types.push(inner);
720                    }
721                    TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })) => {
722                        template_types.push(inner);
723                    }
724                    _ => {}
725                }
726            }
727        }
728
729        template_types
730    }
731
732    pub fn is_objecty(&self) -> bool {
733        for atomic in self.types.as_ref() {
734            if let &TAtomic::Object(_) = atomic {
735                continue;
736            }
737
738            if let TAtomic::Callable(callable) = atomic
739                && callable.get_signature().is_none_or(super::atomic::callable::TCallableSignature::is_closure)
740            {
741                continue;
742            }
743
744            return false;
745        }
746
747        true
748    }
749
750    #[must_use]
751    pub fn is_generator(&self) -> bool {
752        for atomic in self.types.as_ref() {
753            if atomic.is_generator() {
754                continue;
755            }
756
757            return false;
758        }
759
760        true
761    }
762
763    #[must_use]
764    pub fn extends_or_implements(&self, codebase: &CodebaseMetadata, interface: &str) -> bool {
765        for atomic in self.types.as_ref() {
766            if !atomic.extends_or_implements(codebase, interface) {
767                return false;
768            }
769        }
770
771        true
772    }
773
774    #[must_use]
775    pub fn is_generic_parameter(&self) -> bool {
776        self.types.len() == 1 && matches!(self.types[0], TAtomic::GenericParameter(_))
777    }
778
779    #[must_use]
780    pub fn get_generic_parameter_constraint(&self) -> Option<&TUnion> {
781        if self.is_generic_parameter()
782            && let TAtomic::GenericParameter(parameter) = &self.types[0]
783        {
784            return Some(&parameter.constraint);
785        }
786
787        None
788    }
789
790    #[must_use]
791    pub fn is_null(&self) -> bool {
792        self.types.iter().all(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
793    }
794
795    #[must_use]
796    pub fn is_nullable(&self) -> bool {
797        self.types.iter().any(|t| match t {
798            TAtomic::Null => self.types.len() >= 2,
799            TAtomic::GenericParameter(param) => param.constraint.is_nullable(),
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_this()))
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_this()))
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 => 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        const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
1396            .union(UnionFlags::BY_REFERENCE)
1397            .union(UnionFlags::REFERENCE_FREE)
1398            .union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
1399            .union(UnionFlags::POSSIBLY_UNDEFINED)
1400            .union(UnionFlags::IGNORE_NULLABLE_ISSUES)
1401            .union(UnionFlags::IGNORE_FALSABLE_ISSUES)
1402            .union(UnionFlags::FROM_TEMPLATE_DEFAULT);
1403
1404        if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
1405            return false;
1406        }
1407
1408        let len = self.types.len();
1409        if len != other.types.len() {
1410            return false;
1411        }
1412
1413        for i in 0..len {
1414            let mut has_match = false;
1415            for j in 0..len {
1416                if self.types[i] == other.types[j] {
1417                    has_match = true;
1418                    break;
1419                }
1420            }
1421
1422            if !has_match {
1423                return false;
1424            }
1425        }
1426
1427        true
1428    }
1429}
1430
1431pub fn populate_union_type(
1432    unpopulated_union: &mut TUnion,
1433    codebase_symbols: &Symbols,
1434    reference_source: Option<&ReferenceSource>,
1435    symbol_references: &mut SymbolReferences,
1436    force: bool,
1437) {
1438    if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
1439        return;
1440    }
1441
1442    if !unpopulated_union.needs_population() {
1443        return;
1444    }
1445
1446    unpopulated_union.flags.insert(UnionFlags::POPULATED);
1447    let unpopulated_atomics = unpopulated_union.types.to_mut();
1448    for unpopulated_atomic in unpopulated_atomics {
1449        match unpopulated_atomic {
1450            TAtomic::Scalar(TScalar::ClassLikeString(
1451                TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
1452            )) => {
1453                let mut new_constraint = (**constraint).clone();
1454
1455                populate_atomic_type(&mut new_constraint, codebase_symbols, reference_source, symbol_references, force);
1456
1457                **constraint = new_constraint;
1458            }
1459            _ => {
1460                populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
1461            }
1462        }
1463    }
1464}