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_keyed_array(&self) -> bool {
831        self.types.iter().all(TAtomic::is_keyed_array) && !self.types.is_empty()
832    }
833
834    pub fn is_falsable(&self) -> bool {
835        self.types.len() >= 2 && self.types.iter().any(TAtomic::is_false)
836    }
837
838    #[must_use]
839    pub fn has_bool(&self) -> bool {
840        self.types.iter().any(|t| t.is_bool() || t.is_generic_scalar()) && !self.types.is_empty()
841    }
842
843    /// Checks if the union explicitly contains the generic `scalar` type.
844    ///
845    /// This is a specific check for the `scalar` type itself, not for a
846    /// combination of types that would form a scalar (e.g., `int|string|bool|float`).
847    /// For that, see `has_scalar_combination`.
848    pub fn has_scalar(&self) -> bool {
849        self.types.iter().any(TAtomic::is_generic_scalar)
850    }
851
852    /// Checks if the union contains a combination of types that is equivalent
853    /// to the generic `scalar` type (i.e., contains `int`, `float`, `bool`, and `string`).
854    #[must_use]
855    pub fn has_scalar_combination(&self) -> bool {
856        const HAS_INT: u8 = 1 << 0;
857        const HAS_FLOAT: u8 = 1 << 1;
858        const HAS_BOOL: u8 = 1 << 2;
859        const HAS_STRING: u8 = 1 << 3;
860        const ALL_SCALARS: u8 = HAS_INT | HAS_FLOAT | HAS_BOOL | HAS_STRING;
861
862        let mut flags = 0u8;
863
864        for atomic in self.types.as_ref() {
865            if atomic.is_int() {
866                flags |= HAS_INT;
867            } else if atomic.is_float() {
868                flags |= HAS_FLOAT;
869            } else if atomic.is_bool() {
870                flags |= HAS_BOOL;
871            } else if atomic.is_string() {
872                flags |= HAS_STRING;
873            } else if atomic.is_array_key() {
874                flags |= HAS_INT | HAS_STRING;
875            } else if atomic.is_numeric() {
876                // We don't add `string` as `numeric-string` does not contain `string` type
877                flags |= HAS_INT | HAS_FLOAT;
878            } else if atomic.is_generic_scalar() {
879                return true;
880            }
881
882            // Early exit if we've already found all scalar types
883            if flags == ALL_SCALARS {
884                return true;
885            }
886        }
887
888        flags == ALL_SCALARS
889    }
890    pub fn has_array_key(&self) -> bool {
891        self.types.iter().any(TAtomic::is_array_key)
892    }
893
894    pub fn has_iterable(&self) -> bool {
895        self.types.iter().any(TAtomic::is_iterable) && !self.types.is_empty()
896    }
897
898    pub fn has_array(&self) -> bool {
899        self.types.iter().any(TAtomic::is_array) && !self.types.is_empty()
900    }
901
902    #[must_use]
903    pub fn has_traversable(&self, codebase: &CodebaseMetadata) -> bool {
904        self.types.iter().any(|atomic| atomic.is_traversable(codebase)) && !self.types.is_empty()
905    }
906
907    #[must_use]
908    pub fn has_array_key_like(&self) -> bool {
909        self.types.iter().any(|atomic| atomic.is_array_key() || atomic.is_int() || atomic.is_string())
910    }
911
912    pub fn has_numeric(&self) -> bool {
913        self.types.iter().any(TAtomic::is_numeric) && !self.types.is_empty()
914    }
915
916    pub fn is_always_truthy(&self) -> bool {
917        self.types.iter().all(TAtomic::is_truthy) && !self.types.is_empty()
918    }
919
920    pub fn is_always_falsy(&self) -> bool {
921        self.types.iter().all(TAtomic::is_falsy) && !self.types.is_empty()
922    }
923
924    #[must_use]
925    pub fn is_literal_of(&self, other: &TUnion) -> bool {
926        let Some(other_atomic_type) = other.types.first() else {
927            return false;
928        };
929
930        match other_atomic_type {
931            TAtomic::Scalar(TScalar::String(_)) => {
932                for self_atomic_type in self.types.as_ref() {
933                    if self_atomic_type.is_string_of_literal_origin() {
934                        continue;
935                    }
936
937                    return false;
938                }
939
940                true
941            }
942            TAtomic::Scalar(TScalar::Integer(_)) => {
943                for self_atomic_type in self.types.as_ref() {
944                    if self_atomic_type.is_literal_int() {
945                        continue;
946                    }
947
948                    return false;
949                }
950
951                true
952            }
953            TAtomic::Scalar(TScalar::Float(_)) => {
954                for self_atomic_type in self.types.as_ref() {
955                    if self_atomic_type.is_literal_float() {
956                        continue;
957                    }
958
959                    return false;
960                }
961
962                true
963            }
964            _ => false,
965        }
966    }
967
968    #[must_use]
969    pub fn all_literals(&self) -> bool {
970        self.types
971            .iter()
972            .all(|atomic| atomic.is_string_of_literal_origin() || atomic.is_literal_int() || atomic.is_literal_float())
973    }
974
975    #[must_use]
976    pub fn has_static_object(&self) -> bool {
977        self.types
978            .iter()
979            .any(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_this()))
980    }
981
982    #[must_use]
983    pub fn is_static_object(&self) -> bool {
984        self.types
985            .iter()
986            .all(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_this()))
987    }
988
989    #[inline]
990    #[must_use]
991    pub fn is_single(&self) -> bool {
992        self.types.len() == 1
993    }
994
995    #[inline]
996    #[must_use]
997    pub fn get_single_string(&self) -> Option<&TString> {
998        if self.is_single()
999            && let TAtomic::Scalar(TScalar::String(string)) = &self.types[0]
1000        {
1001            Some(string)
1002        } else {
1003            None
1004        }
1005    }
1006
1007    #[inline]
1008    #[must_use]
1009    pub fn get_single_array(&self) -> Option<&TArray> {
1010        if self.is_single()
1011            && let TAtomic::Array(array) = &self.types[0]
1012        {
1013            Some(array)
1014        } else {
1015            None
1016        }
1017    }
1018
1019    #[inline]
1020    #[must_use]
1021    pub fn get_single_bool(&self) -> Option<&TBool> {
1022        if self.is_single()
1023            && let TAtomic::Scalar(TScalar::Bool(bool)) = &self.types[0]
1024        {
1025            Some(bool)
1026        } else {
1027            None
1028        }
1029    }
1030
1031    #[inline]
1032    #[must_use]
1033    pub fn get_single_named_object(&self) -> Option<&TNamedObject> {
1034        if self.is_single()
1035            && let TAtomic::Object(TObject::Named(named_object)) = &self.types[0]
1036        {
1037            Some(named_object)
1038        } else {
1039            None
1040        }
1041    }
1042
1043    #[inline]
1044    #[must_use]
1045    pub fn get_single_shaped_object(&self) -> Option<&TObjectWithProperties> {
1046        if self.is_single()
1047            && let TAtomic::Object(TObject::WithProperties(shaped_object)) = &self.types[0]
1048        {
1049            Some(shaped_object)
1050        } else {
1051            None
1052        }
1053    }
1054
1055    #[inline]
1056    #[must_use]
1057    pub fn get_single(&self) -> &TAtomic {
1058        &self.types[0]
1059    }
1060
1061    #[inline]
1062    #[must_use]
1063    pub fn get_single_owned(self) -> TAtomic {
1064        self.types[0].clone()
1065    }
1066
1067    #[inline]
1068    #[must_use]
1069    pub fn is_named_object(&self) -> bool {
1070        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1071    }
1072
1073    #[must_use]
1074    pub fn is_enum(&self) -> bool {
1075        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_))))
1076    }
1077
1078    #[must_use]
1079    pub fn is_enum_case(&self) -> bool {
1080        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1081    }
1082
1083    #[must_use]
1084    pub fn is_single_enum_case(&self) -> bool {
1085        self.is_single()
1086            && self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1087    }
1088
1089    #[inline]
1090    #[must_use]
1091    pub fn has_named_object(&self) -> bool {
1092        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1093    }
1094
1095    #[inline]
1096    #[must_use]
1097    pub fn has_object(&self) -> bool {
1098        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Any | TObject::WithProperties(_))))
1099    }
1100
1101    #[inline]
1102    #[must_use]
1103    pub fn has_callable(&self) -> bool {
1104        self.types.iter().any(|t| matches!(t, TAtomic::Callable(_)))
1105    }
1106
1107    #[inline]
1108    #[must_use]
1109    pub fn is_callable(&self) -> bool {
1110        self.types.iter().all(|t| matches!(t, TAtomic::Callable(_)))
1111    }
1112
1113    #[inline]
1114    #[must_use]
1115    pub fn has_object_type(&self) -> bool {
1116        self.types.iter().any(|t| matches!(t, TAtomic::Object(_)))
1117    }
1118
1119    /// Return a vector of pairs containing the enum name, and their case name
1120    /// if specified.
1121    #[must_use]
1122    pub fn get_enum_cases(&self) -> Vec<(Atom, Option<Atom>)> {
1123        self.types
1124            .iter()
1125            .filter_map(|t| match t {
1126                TAtomic::Object(TObject::Enum(enum_object)) => Some((enum_object.name, enum_object.case)),
1127                _ => None,
1128            })
1129            .collect()
1130    }
1131
1132    #[must_use]
1133    pub fn get_single_int(&self) -> Option<TInteger> {
1134        if self.is_single() { self.get_single().get_integer() } else { None }
1135    }
1136
1137    #[must_use]
1138    pub fn get_single_literal_int_value(&self) -> Option<i64> {
1139        if self.is_single() { self.get_single().get_literal_int_value() } else { None }
1140    }
1141
1142    #[must_use]
1143    pub fn get_single_maximum_int_value(&self) -> Option<i64> {
1144        if self.is_single() { self.get_single().get_maximum_int_value() } else { None }
1145    }
1146
1147    #[must_use]
1148    pub fn get_single_minimum_int_value(&self) -> Option<i64> {
1149        if self.is_single() { self.get_single().get_minimum_int_value() } else { None }
1150    }
1151
1152    #[must_use]
1153    pub fn get_single_literal_float_value(&self) -> Option<f64> {
1154        if self.is_single() { self.get_single().get_literal_float_value() } else { None }
1155    }
1156
1157    #[must_use]
1158    pub fn get_single_literal_string_value(&self) -> Option<&str> {
1159        if self.is_single() { self.get_single().get_literal_string_value() } else { None }
1160    }
1161
1162    #[must_use]
1163    pub fn get_single_class_string_value(&self) -> Option<Atom> {
1164        if self.is_single() { self.get_single().get_class_string_value() } else { None }
1165    }
1166
1167    #[must_use]
1168    pub fn get_single_array_key(&self) -> Option<ArrayKey> {
1169        if self.is_single() { self.get_single().to_array_key() } else { None }
1170    }
1171
1172    #[must_use]
1173    pub fn get_single_key_of_array_like(&self) -> Option<TUnion> {
1174        if !self.is_single() {
1175            return None;
1176        }
1177
1178        match self.get_single() {
1179            TAtomic::Array(array) => match array {
1180                TArray::List(_) => Some(get_int()),
1181                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1182                    Some((k, _)) => Some(*k.clone()),
1183                    None => Some(get_arraykey()),
1184                },
1185            },
1186            _ => None,
1187        }
1188    }
1189
1190    #[must_use]
1191    pub fn get_single_value_of_array_like(&self) -> Option<Cow<'_, TUnion>> {
1192        if !self.is_single() {
1193            return None;
1194        }
1195
1196        match self.get_single() {
1197            TAtomic::Array(array) => match array {
1198                TArray::List(list) => Some(Cow::Borrowed(&list.element_type)),
1199                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1200                    Some((_, v)) => Some(Cow::Borrowed(v)),
1201                    None => Some(Cow::Owned(get_mixed())),
1202                },
1203            },
1204            _ => None,
1205        }
1206    }
1207
1208    #[must_use]
1209    pub fn get_literal_ints(&self) -> Vec<&TAtomic> {
1210        self.types.iter().filter(|a| a.is_literal_int()).collect()
1211    }
1212
1213    #[must_use]
1214    pub fn get_literal_strings(&self) -> Vec<&TAtomic> {
1215        self.types.iter().filter(|a| a.is_known_literal_string()).collect()
1216    }
1217
1218    #[must_use]
1219    pub fn get_literal_string_values(&self) -> Vec<Option<Atom>> {
1220        self.get_literal_strings()
1221            .into_iter()
1222            .map(|atom| match atom {
1223                TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(value)), .. })) => {
1224                    Some(*value)
1225                }
1226                _ => None,
1227            })
1228            .collect()
1229    }
1230
1231    #[must_use]
1232    pub fn has_literal_float(&self) -> bool {
1233        self.types.iter().any(|atomic| match atomic {
1234            TAtomic::Scalar(scalar) => scalar.is_literal_float(),
1235            _ => false,
1236        })
1237    }
1238
1239    #[must_use]
1240    pub fn has_literal_int(&self) -> bool {
1241        self.types.iter().any(|atomic| match atomic {
1242            TAtomic::Scalar(scalar) => scalar.is_literal_int(),
1243            _ => false,
1244        })
1245    }
1246
1247    #[must_use]
1248    pub fn has_literal_string(&self) -> bool {
1249        self.types.iter().any(|atomic| match atomic {
1250            TAtomic::Scalar(scalar) => scalar.is_known_literal_string(),
1251            _ => false,
1252        })
1253    }
1254
1255    #[must_use]
1256    pub fn has_literal_value(&self) -> bool {
1257        self.types.iter().any(|atomic| match atomic {
1258            TAtomic::Scalar(scalar) => scalar.is_literal_value(),
1259            _ => false,
1260        })
1261    }
1262
1263    #[must_use]
1264    pub fn accepts_false(&self) -> bool {
1265        self.types.iter().any(|t| match t {
1266            TAtomic::GenericParameter(parameter) => parameter.constraint.accepts_false(),
1267            TAtomic::Mixed(mixed) if !mixed.is_truthy() => true,
1268            TAtomic::Scalar(TScalar::Generic | TScalar::Bool(TBool { value: None | Some(false) })) => true,
1269            _ => false,
1270        })
1271    }
1272
1273    #[must_use]
1274    pub fn accepts_null(&self) -> bool {
1275        self.types.iter().any(|t| match t {
1276            TAtomic::GenericParameter(generic_parameter) => generic_parameter.constraint.accepts_null(),
1277            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
1278            TAtomic::Null => true,
1279            _ => false,
1280        })
1281    }
1282}
1283
1284impl TType for TUnion {
1285    fn get_child_nodes(&self) -> Vec<TypeRef<'_>> {
1286        self.types.iter().map(TypeRef::Atomic).collect()
1287    }
1288
1289    fn needs_population(&self) -> bool {
1290        !self.flags.contains(UnionFlags::POPULATED) && self.types.iter().any(super::TType::needs_population)
1291    }
1292
1293    fn is_expandable(&self) -> bool {
1294        if self.types.is_empty() {
1295            return true;
1296        }
1297
1298        self.types.iter().any(super::TType::is_expandable)
1299    }
1300
1301    fn is_complex(&self) -> bool {
1302        self.types.len() > 3 || self.types.iter().any(super::TType::is_complex)
1303    }
1304
1305    fn get_id(&self) -> Atom {
1306        let len = self.types.len();
1307
1308        let mut atomic_ids: Vec<Atom> = self
1309            .types
1310            .as_ref()
1311            .iter()
1312            .map(|atomic| {
1313                let id = atomic.get_id();
1314                if atomic.is_generic_parameter() || atomic.has_intersection_types() && len > 1 {
1315                    concat_atom!("(", id.as_str(), ")")
1316                } else {
1317                    id
1318                }
1319            })
1320            .collect();
1321
1322        if len <= 1 {
1323            return atomic_ids.pop().unwrap_or_else(empty_atom);
1324        }
1325
1326        atomic_ids.sort_unstable();
1327        let mut result = atomic_ids[0];
1328        for id in &atomic_ids[1..] {
1329            result = concat_atom!(result.as_str(), "|", id.as_str());
1330        }
1331
1332        result
1333    }
1334
1335    fn get_pretty_id_with_indent(&self, indent: usize) -> Atom {
1336        let len = self.types.len();
1337
1338        if len <= 1 {
1339            return self.types.first().map_or_else(empty_atom, |atomic| atomic.get_pretty_id_with_indent(indent));
1340        }
1341
1342        // Use multiline format for unions with more than 3 types
1343        if len > 3 {
1344            let mut atomic_ids: Vec<Atom> = self
1345                .types
1346                .as_ref()
1347                .iter()
1348                .map(|atomic| {
1349                    let id = atomic.get_pretty_id_with_indent(indent + 2);
1350                    if atomic.has_intersection_types() { concat_atom!("(", id.as_str(), ")") } else { id }
1351                })
1352                .collect();
1353
1354            atomic_ids.sort_unstable();
1355
1356            let mut result = String::new();
1357            result += &atomic_ids[0];
1358            for id in &atomic_ids[1..] {
1359                result += "\n";
1360                result += &" ".repeat(indent);
1361                result += "| ";
1362                result += id.as_str();
1363            }
1364
1365            atom(&result)
1366        } else {
1367            // Use inline format for smaller unions
1368            let mut atomic_ids: Vec<Atom> = self
1369                .types
1370                .as_ref()
1371                .iter()
1372                .map(|atomic| {
1373                    let id = atomic.get_pretty_id_with_indent(indent);
1374                    if atomic.has_intersection_types() && len > 1 { concat_atom!("(", id.as_str(), ")") } else { id }
1375                })
1376                .collect();
1377
1378            atomic_ids.sort_unstable();
1379            let mut result = atomic_ids[0];
1380            for id in &atomic_ids[1..] {
1381                result = concat_atom!(result.as_str(), " | ", id.as_str());
1382            }
1383
1384            result
1385        }
1386    }
1387}
1388
1389impl PartialEq for TUnion {
1390    fn eq(&self, other: &TUnion) -> bool {
1391        const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
1392            .union(UnionFlags::BY_REFERENCE)
1393            .union(UnionFlags::REFERENCE_FREE)
1394            .union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
1395            .union(UnionFlags::POSSIBLY_UNDEFINED)
1396            .union(UnionFlags::IGNORE_NULLABLE_ISSUES)
1397            .union(UnionFlags::IGNORE_FALSABLE_ISSUES)
1398            .union(UnionFlags::FROM_TEMPLATE_DEFAULT);
1399
1400        if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
1401            return false;
1402        }
1403
1404        let len = self.types.len();
1405        if len != other.types.len() {
1406            return false;
1407        }
1408
1409        for i in 0..len {
1410            let mut has_match = false;
1411            for j in 0..len {
1412                if self.types[i] == other.types[j] {
1413                    has_match = true;
1414                    break;
1415                }
1416            }
1417
1418            if !has_match {
1419                return false;
1420            }
1421        }
1422
1423        true
1424    }
1425}
1426
1427pub fn populate_union_type(
1428    unpopulated_union: &mut TUnion,
1429    codebase_symbols: &Symbols,
1430    reference_source: Option<&ReferenceSource>,
1431    symbol_references: &mut SymbolReferences,
1432    force: bool,
1433) {
1434    if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
1435        return;
1436    }
1437
1438    if !unpopulated_union.needs_population() {
1439        return;
1440    }
1441
1442    unpopulated_union.flags.insert(UnionFlags::POPULATED);
1443    let unpopulated_atomics = unpopulated_union.types.to_mut();
1444    for unpopulated_atomic in unpopulated_atomics {
1445        match unpopulated_atomic {
1446            TAtomic::Scalar(TScalar::ClassLikeString(
1447                TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
1448            )) => {
1449                let mut new_constraint = (**constraint).clone();
1450
1451                populate_atomic_type(&mut new_constraint, codebase_symbols, reference_source, symbol_references, force);
1452
1453                **constraint = new_constraint;
1454            }
1455            _ => {
1456                populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
1457            }
1458        }
1459    }
1460}