mago_codex/ttype/
union.rs

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