Skip to main content

mago_codex/ttype/
union.rs

1use std::borrow::Cow;
2use std::hash::Hash;
3use std::hash::Hasher;
4use std::sync::Arc;
5
6use mago_word::Word;
7use mago_word::concat_word;
8use mago_word::empty_word;
9use mago_word::word;
10
11use crate::metadata::CodebaseMetadata;
12use crate::reference::ReferenceSource;
13use crate::reference::SymbolReferences;
14use crate::symbol::Symbols;
15use crate::ttype::TType;
16use crate::ttype::TypeRef;
17use crate::ttype::atomic::TAtomic;
18use crate::ttype::atomic::array::TArray;
19use crate::ttype::atomic::array::key::ArrayKey;
20use crate::ttype::atomic::generic::TGenericParameter;
21use crate::ttype::atomic::mixed::truthiness::TMixedTruthiness;
22use crate::ttype::atomic::object::TObject;
23use crate::ttype::atomic::object::named::TNamedObject;
24use crate::ttype::atomic::object::with_properties::TObjectWithProperties;
25use crate::ttype::atomic::populate_atomic_type;
26use crate::ttype::atomic::scalar::TScalar;
27use crate::ttype::atomic::scalar::bool::TBool;
28use crate::ttype::atomic::scalar::class_like_string::TClassLikeString;
29use crate::ttype::atomic::scalar::float::TFloat;
30use crate::ttype::atomic::scalar::int::TInteger;
31use crate::ttype::atomic::scalar::string::TString;
32use crate::ttype::atomic::scalar::string::TStringCasing;
33use crate::ttype::atomic::scalar::string::TStringLiteral;
34use crate::ttype::flags::UnionFlags;
35use crate::ttype::get_arraykey;
36use crate::ttype::get_int;
37use crate::ttype::get_mixed;
38
39#[derive(Debug, Clone, Eq, PartialOrd, Ord)]
40#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
41pub struct TUnion {
42    pub types: Cow<'static, [TAtomic]>,
43    pub flags: UnionFlags,
44}
45
46impl Hash for TUnion {
47    fn hash<H>(&self, state: &mut H)
48    where
49        H: Hasher,
50    {
51        for t in self.types.as_ref() {
52            t.hash(state);
53        }
54    }
55}
56
57impl TUnion {
58    /// The primary constructor for creating a `TUnion` from a Cow.
59    ///
60    /// This is the most basic way to create a `TUnion` and is used by both the
61    /// zero-allocation static helpers and the `from_vec` constructor.
62    #[must_use]
63    pub fn new(types: Cow<'static, [TAtomic]>) -> TUnion {
64        TUnion { types, flags: UnionFlags::empty() }
65    }
66
67    /// Creates a `TUnion` from an owned Vec, performing necessary cleanup.
68    ///
69    /// This preserves the original logic for cleaning up dynamically created unions,
70    /// such as removing redundant `never` types.
71    ///
72    /// Atoms are additionally sorted into canonical order so that two unions
73    /// built from the same set of atoms compare equal via the ordered
74    /// slice-equality fast path in [`PartialEq::eq`], without triggering the
75    /// fallback O(N^2) subset check.
76    ///
77    /// # Panics
78    ///
79    /// In debug builds, panics if:
80    /// - The input Vec is empty (unions must contain at least one type)
81    /// - The input contains a mix of `never` types with other types (invalid union construction)
82    #[must_use]
83    pub fn from_vec(mut types: Vec<TAtomic>) -> TUnion {
84        if cfg!(debug_assertions) {
85            assert!(
86                !types.is_empty(),
87                "TUnion::from_vec() received an empty Vec. This indicates a logic error \
88                 in type construction - unions must contain at least one type. \
89                 Consider using TAtomic::Never for empty/impossible types."
90            );
91        }
92
93        // If we have more than one type, 'never' is redundant and can be removed,
94        // as the union `A|never` is simply `A`.
95        if types.len() > 1 {
96            types.retain(|atomic| {
97                !atomic.is_never() && !atomic.map_generic_parameter_constraint(TUnion::is_never).unwrap_or(false)
98            });
99        }
100
101        // If the vector was originally empty, or contained only 'never' types
102        // which were removed, ensure the final union is `never`.
103        if types.is_empty() {
104            types.push(TAtomic::Never);
105        }
106
107        if types.len() > 1 {
108            types.sort_unstable();
109        }
110
111        Self::new(Cow::Owned(types))
112    }
113
114    /// Creates a `TUnion` from a single atomic type, which can be either
115    /// borrowed from a static source or owned.
116    ///
117    /// This function is a key optimization point. When passed a `Cow::Borrowed`,
118    /// it creates the `TUnion` without any heap allocation.
119    #[must_use]
120    pub fn from_single(atomic: Cow<'static, TAtomic>) -> TUnion {
121        let types_cow = match atomic {
122            Cow::Borrowed(borrowed_atomic) => Cow::Borrowed(std::slice::from_ref(borrowed_atomic)),
123            Cow::Owned(owned_atomic) => Cow::Owned(vec![owned_atomic]),
124        };
125
126        TUnion::new(types_cow)
127    }
128
129    /// Creates a `TUnion` from a single owned atomic type.
130    #[must_use]
131    pub fn from_atomic(atomic: TAtomic) -> TUnion {
132        TUnion::new(Cow::Owned(vec![atomic]))
133    }
134
135    #[inline]
136    pub fn set_possibly_undefined(&mut self, possibly_undefined: bool, from_try: Option<bool>) {
137        let from_try = from_try.unwrap_or(self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY));
138
139        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED, possibly_undefined);
140        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, from_try);
141    }
142
143    #[inline]
144    #[must_use]
145    pub const fn had_template(&self) -> bool {
146        self.flags.contains(UnionFlags::HAD_TEMPLATE)
147    }
148
149    #[inline]
150    #[must_use]
151    pub const fn by_reference(&self) -> bool {
152        self.flags.contains(UnionFlags::BY_REFERENCE)
153    }
154
155    #[inline]
156    #[must_use]
157    pub const fn reference_free(&self) -> bool {
158        self.flags.contains(UnionFlags::REFERENCE_FREE)
159    }
160
161    #[inline]
162    #[must_use]
163    pub const fn possibly_undefined_from_try(&self) -> bool {
164        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
165    }
166
167    #[inline]
168    #[must_use]
169    pub const fn possibly_undefined(&self) -> bool {
170        self.flags.contains(UnionFlags::POSSIBLY_UNDEFINED)
171    }
172
173    #[inline]
174    #[must_use]
175    pub const fn ignore_nullable_issues(&self) -> bool {
176        self.flags.contains(UnionFlags::IGNORE_NULLABLE_ISSUES)
177    }
178
179    #[inline]
180    #[must_use]
181    pub const fn ignore_falsable_issues(&self) -> bool {
182        self.flags.contains(UnionFlags::IGNORE_FALSABLE_ISSUES)
183    }
184
185    #[inline]
186    #[must_use]
187    pub const fn from_template_default(&self) -> bool {
188        self.flags.contains(UnionFlags::FROM_TEMPLATE_DEFAULT)
189    }
190
191    #[inline]
192    #[must_use]
193    pub const fn populated(&self) -> bool {
194        self.flags.contains(UnionFlags::POPULATED)
195    }
196
197    #[inline]
198    #[must_use]
199    pub const fn has_nullsafe_null(&self) -> bool {
200        self.flags.contains(UnionFlags::NULLSAFE_NULL)
201    }
202
203    #[inline]
204    pub fn set_had_template(&mut self, value: bool) {
205        self.flags.set(UnionFlags::HAD_TEMPLATE, value);
206    }
207
208    #[inline]
209    pub fn set_by_reference(&mut self, value: bool) {
210        self.flags.set(UnionFlags::BY_REFERENCE, value);
211    }
212
213    #[inline]
214    pub fn set_reference_free(&mut self, value: bool) {
215        self.flags.set(UnionFlags::REFERENCE_FREE, value);
216    }
217
218    #[inline]
219    pub fn set_possibly_undefined_from_try(&mut self, value: bool) {
220        self.flags.set(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY, value);
221    }
222
223    #[inline]
224    pub fn set_ignore_nullable_issues(&mut self, value: bool) {
225        self.flags.set(UnionFlags::IGNORE_NULLABLE_ISSUES, value);
226    }
227
228    #[inline]
229    pub fn set_ignore_falsable_issues(&mut self, value: bool) {
230        self.flags.set(UnionFlags::IGNORE_FALSABLE_ISSUES, value);
231    }
232
233    #[inline]
234    pub fn set_from_template_default(&mut self, value: bool) {
235        self.flags.set(UnionFlags::FROM_TEMPLATE_DEFAULT, value);
236    }
237
238    #[inline]
239    pub fn set_populated(&mut self, value: bool) {
240        self.flags.set(UnionFlags::POPULATED, value);
241    }
242
243    #[inline]
244    pub fn set_nullsafe_null(&mut self, value: bool) {
245        self.flags.set(UnionFlags::NULLSAFE_NULL, value);
246    }
247
248    /// Creates a new `TUnion` with the same properties as the original, but with a new set of types.
249    #[must_use]
250    pub fn clone_with_types(&self, types: Vec<TAtomic>) -> TUnion {
251        TUnion { types: Cow::Owned(ensure_non_empty_types(types)), flags: self.flags }
252    }
253
254    #[must_use]
255    pub fn to_non_nullable(&self) -> TUnion {
256        TUnion {
257            types: Cow::Owned(ensure_non_empty_types(self.get_non_nullable_types())),
258            flags: self.flags & !UnionFlags::NULLSAFE_NULL,
259        }
260    }
261
262    #[must_use]
263    pub fn to_truthy(&self) -> TUnion {
264        TUnion { types: Cow::Owned(ensure_non_empty_types(self.get_truthy_types())), flags: self.flags }
265    }
266
267    #[must_use]
268    pub fn get_non_nullable_types(&self) -> Vec<TAtomic> {
269        self.types
270            .iter()
271            .filter_map(|t| match t {
272                TAtomic::Null | TAtomic::Void => None,
273                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
274                    parameter_name: parameter.parameter_name,
275                    defining_entity: parameter.defining_entity,
276                    intersection_types: parameter.intersection_types.clone(),
277                    constraint: Arc::new(parameter.constraint.to_non_nullable()),
278                })),
279                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_is_non_null(true))),
280                atomic => Some(atomic.clone()),
281            })
282            .collect()
283    }
284
285    #[must_use]
286    pub fn get_truthy_types(&self) -> Vec<TAtomic> {
287        self.types
288            .iter()
289            .filter_map(|t| match t {
290                TAtomic::GenericParameter(parameter) => Some(TAtomic::GenericParameter(TGenericParameter {
291                    parameter_name: parameter.parameter_name,
292                    defining_entity: parameter.defining_entity,
293                    intersection_types: parameter.intersection_types.clone(),
294                    constraint: Arc::new(parameter.constraint.to_truthy()),
295                })),
296                TAtomic::Mixed(mixed) => Some(TAtomic::Mixed(mixed.with_truthiness(TMixedTruthiness::Truthy))),
297                atomic => {
298                    if atomic.is_falsy() {
299                        None
300                    } else {
301                        Some(atomic.clone())
302                    }
303                }
304            })
305            .collect()
306    }
307
308    /// Recursively replaces every narrowed scalar atom in this union with its
309    /// general form: any string narrowing -> `string`, any integer narrowing
310    /// (literal, range, non-negative-int, ...) -> `int`, any float narrowing
311    /// -> `float`, `true`/`false` -> `bool`.
312    ///
313    /// Descends into nested type holders (array elements, object type
314    /// parameters, iterable key/value, generic parameter constraints,
315    /// conditional branches, intersection types, ...) so a narrowing nested
316    /// arbitrarily deep in a callee-mutable structure is also widened.
317    pub fn widen_scalars(&mut self) {
318        for atomic in self.types.to_mut() {
319            widen_atomic_scalars(atomic);
320        }
321    }
322
323    /// Recursively replaces only *literal* scalar atoms in this union with
324    /// their general form: literal strings -> `string`, literal ints -> `int`,
325    /// literal floats -> `float`, `true`/`false` -> `bool`. Unlike
326    /// [`Self::widen_scalars`], user-declared narrowings such as
327    /// `non-negative-int`, `non-empty-string`, or `int<1, max>` are preserved.
328    pub fn widen_literals(&mut self) {
329        for atomic in self.types.to_mut() {
330            widen_atomic_literals(atomic);
331        }
332    }
333
334    /// Adds `null` to the union type, making it nullable.
335    #[must_use]
336    pub fn as_nullable(mut self) -> TUnion {
337        let types = self.types.to_mut();
338
339        for atomic in types.iter_mut() {
340            if let TAtomic::Mixed(mixed) = atomic {
341                *mixed = mixed.with_is_non_null(false);
342            }
343        }
344
345        if !types.iter().any(|atomic| atomic.is_null() || atomic.is_mixed()) {
346            types.push(TAtomic::Null);
347        }
348
349        self
350    }
351
352    /// Removes a specific atomic type from the union.
353    pub fn remove_type(&mut self, bad_type: &TAtomic) {
354        self.types.to_mut().retain(|t| t != bad_type);
355    }
356
357    /// Replaces a specific atomic type in the union with a new type.
358    pub fn replace_type(&mut self, remove_type: &TAtomic, add_type: TAtomic) {
359        let types = self.types.to_mut();
360
361        if let Some(index) = types.iter().position(|t| t == remove_type) {
362            types[index] = add_type;
363        } else {
364            types.push(add_type);
365        }
366    }
367
368    #[must_use]
369    pub fn is_int(&self) -> bool {
370        for atomic in self.types.as_ref() {
371            if !atomic.is_int() {
372                return false;
373            }
374        }
375
376        true
377    }
378
379    #[must_use]
380    pub fn has_int_or_float(&self) -> bool {
381        for atomic in self.types.as_ref() {
382            if atomic.is_int_or_float() {
383                return true;
384            }
385        }
386
387        false
388    }
389
390    #[must_use]
391    pub fn has_int_and_float(&self) -> bool {
392        let mut has_int = false;
393        let mut has_float = false;
394
395        for atomic in self.types.as_ref() {
396            if atomic.is_int() {
397                has_int = true;
398            } else if atomic.is_float() {
399                has_float = true;
400            } else if atomic.is_int_or_float() {
401                has_int = true;
402                has_float = true;
403            }
404
405            if has_int && has_float {
406                return true;
407            }
408        }
409
410        false
411    }
412
413    #[must_use]
414    pub fn has_int_and_string(&self) -> bool {
415        let mut has_int = false;
416        let mut has_string = false;
417
418        for atomic in self.types.as_ref() {
419            if atomic.is_int() {
420                has_int = true;
421            } else if atomic.is_string() {
422                has_string = true;
423            } else if atomic.is_array_key() {
424                has_int = true;
425                has_string = true;
426            }
427
428            if has_int && has_string {
429                return true;
430            }
431        }
432
433        false
434    }
435
436    #[must_use]
437    pub fn has_int(&self) -> bool {
438        for atomic in self.types.as_ref() {
439            if atomic.is_int() || atomic.is_array_key() || atomic.is_numeric() {
440                return true;
441            }
442        }
443
444        false
445    }
446
447    #[must_use]
448    pub fn has_float(&self) -> bool {
449        for atomic in self.types.as_ref() {
450            if atomic.is_float() {
451                return true;
452            }
453        }
454
455        false
456    }
457
458    #[must_use]
459    pub fn is_array_key(&self) -> bool {
460        for atomic in self.types.as_ref() {
461            if atomic.is_array_key() {
462                continue;
463            }
464
465            return false;
466        }
467
468        true
469    }
470
471    #[must_use]
472    pub fn is_any_string(&self) -> bool {
473        for atomic in self.types.as_ref() {
474            if !atomic.is_any_string() {
475                return false;
476            }
477        }
478
479        true
480    }
481
482    pub fn is_string(&self) -> bool {
483        self.types.iter().all(TAtomic::is_string) && !self.types.is_empty()
484    }
485
486    #[must_use]
487    pub fn is_always_array_key(&self, ignore_never: bool) -> bool {
488        self.types.iter().all(|atomic| match atomic {
489            TAtomic::Never => ignore_never,
490            TAtomic::Scalar(scalar) => matches!(
491                scalar,
492                TScalar::ArrayKey | TScalar::Integer(_) | TScalar::String(_) | TScalar::ClassLikeString(_)
493            ),
494            TAtomic::GenericParameter(generic_parameter) => {
495                generic_parameter.constraint.is_always_array_key(ignore_never)
496            }
497            _ => false,
498        })
499    }
500
501    pub fn is_non_empty_string(&self) -> bool {
502        self.types.iter().all(TAtomic::is_non_empty_string) && !self.types.is_empty()
503    }
504
505    pub fn is_empty_array(&self) -> bool {
506        self.types.iter().all(TAtomic::is_empty_array) && !self.types.is_empty()
507    }
508
509    pub fn has_string(&self) -> bool {
510        self.types.iter().any(TAtomic::is_string) && !self.types.is_empty()
511    }
512
513    pub fn is_float(&self) -> bool {
514        self.types.iter().all(TAtomic::is_float) && !self.types.is_empty()
515    }
516
517    pub fn is_bool(&self) -> bool {
518        self.types.iter().all(TAtomic::is_bool) && !self.types.is_empty()
519    }
520
521    pub fn is_never(&self) -> bool {
522        self.types.iter().all(TAtomic::is_never) || self.types.is_empty()
523    }
524
525    pub fn is_never_template(&self) -> bool {
526        self.types.iter().all(TAtomic::is_templated_as_never) && !self.types.is_empty()
527    }
528
529    #[must_use]
530    pub fn is_placeholder(&self) -> bool {
531        self.types.iter().all(|t| matches!(t, TAtomic::Placeholder)) && !self.types.is_empty()
532    }
533
534    /// Returns true if this union or any type parameter within it contains a Placeholder.
535    #[must_use]
536    pub fn contains_placeholder(&self) -> bool {
537        self.types.iter().any(|t| t.contains_placeholder())
538    }
539
540    pub fn is_true(&self) -> bool {
541        self.types.iter().all(TAtomic::is_true) && !self.types.is_empty()
542    }
543
544    pub fn is_false(&self) -> bool {
545        self.types.iter().all(TAtomic::is_false) && !self.types.is_empty()
546    }
547
548    #[must_use]
549    pub fn is_nonnull(&self) -> bool {
550        self.types.len() == 1 && matches!(self.types[0], TAtomic::Mixed(mixed) if mixed.is_non_null())
551    }
552
553    pub fn is_numeric(&self) -> bool {
554        self.types.iter().all(TAtomic::is_numeric) && !self.types.is_empty()
555    }
556
557    pub fn is_int_or_float(&self) -> bool {
558        self.types.iter().all(TAtomic::is_int_or_float) && !self.types.is_empty()
559    }
560
561    /// Returns `Some(true)` if all types are effectively int, `Some(false)` if all are effectively float,
562    /// or `None` if mixed or neither. Handles unions like `1|2` (all int) or `3.4|4.5` (all float).
563    #[must_use]
564    pub fn effective_int_or_float(&self) -> Option<bool> {
565        let mut result: Option<bool> = None;
566        for atomic in self.types.as_ref() {
567            {
568                let is_int = atomic.effective_int_or_float()?;
569                if let Some(prev) = result {
570                    if prev != is_int {
571                        return None;
572                    }
573                } else {
574                    result = Some(is_int);
575                }
576            }
577        }
578
579        result
580    }
581
582    #[must_use]
583    pub fn is_mixed(&self) -> bool {
584        self.types.iter().all(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
585    }
586
587    pub fn is_mixed_template(&self) -> bool {
588        self.types.iter().all(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
589    }
590
591    #[must_use]
592    pub fn has_mixed(&self) -> bool {
593        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(_))) && !self.types.is_empty()
594    }
595
596    pub fn has_mixed_template(&self) -> bool {
597        self.types.iter().any(TAtomic::is_templated_as_mixed) && !self.types.is_empty()
598    }
599
600    #[must_use]
601    pub fn has_nullable_mixed(&self) -> bool {
602        self.types.iter().any(|t| matches!(t, TAtomic::Mixed(mixed) if !mixed.is_non_null())) && !self.types.is_empty()
603    }
604
605    #[must_use]
606    pub fn has_void(&self) -> bool {
607        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
608    }
609
610    #[must_use]
611    pub fn has_null(&self) -> bool {
612        self.types.iter().any(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
613    }
614
615    #[must_use]
616    pub fn has_nullish(&self) -> bool {
617        self.types.iter().any(|t| match t {
618            TAtomic::Null | TAtomic::Void => true,
619            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
620            TAtomic::GenericParameter(parameter) => parameter.constraint.has_nullish(),
621            _ => false,
622        }) && !self.types.is_empty()
623    }
624
625    #[must_use]
626    pub fn is_nullable_mixed(&self) -> bool {
627        if self.types.len() != 1 {
628            return false;
629        }
630
631        match &self.types[0] {
632            TAtomic::Mixed(mixed) => !mixed.is_non_null(),
633            _ => false,
634        }
635    }
636
637    #[must_use]
638    pub fn is_falsy_mixed(&self) -> bool {
639        if self.types.len() != 1 {
640            return false;
641        }
642
643        matches!(&self.types[0], &TAtomic::Mixed(mixed) if mixed.is_falsy())
644    }
645
646    #[must_use]
647    pub fn is_vanilla_mixed(&self) -> bool {
648        if self.types.len() != 1 {
649            return false;
650        }
651
652        self.types[0].is_vanilla_mixed()
653    }
654
655    #[must_use]
656    pub fn is_templated_as_vanilla_mixed(&self) -> bool {
657        if self.types.len() != 1 {
658            return false;
659        }
660
661        self.types[0].is_templated_as_vanilla_mixed()
662    }
663
664    #[must_use]
665    pub fn has_template_or_static(&self) -> bool {
666        for atomic in self.types.as_ref() {
667            if let TAtomic::GenericParameter(_) = atomic {
668                return true;
669            }
670
671            if let TAtomic::Object(TObject::Named(named_object)) = atomic {
672                if named_object.is_static {
673                    return true;
674                }
675
676                if let Some(intersections) = named_object.get_intersection_types() {
677                    for intersection in intersections {
678                        if let TAtomic::GenericParameter(_) = intersection {
679                            return true;
680                        }
681                    }
682                }
683            }
684        }
685
686        false
687    }
688
689    #[must_use]
690    pub fn has_template(&self) -> bool {
691        for atomic in self.types.as_ref() {
692            if let TAtomic::GenericParameter(_) = atomic {
693                return true;
694            }
695
696            if let Some(intersections) = atomic.get_intersection_types() {
697                for intersection in intersections {
698                    if let TAtomic::GenericParameter(_) = intersection {
699                        return true;
700                    }
701                }
702            }
703        }
704
705        false
706    }
707
708    #[must_use]
709    pub fn has_template_types(&self) -> bool {
710        let all_child_nodes = self.get_all_child_nodes();
711
712        for child_node in all_child_nodes {
713            if let TypeRef::Atomic(
714                TAtomic::GenericParameter(_)
715                | TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })),
716            ) = child_node
717            {
718                return true;
719            }
720        }
721
722        false
723    }
724
725    #[must_use]
726    pub fn get_template_types(&self) -> Vec<&TAtomic> {
727        let all_child_nodes = self.get_all_child_nodes();
728
729        let mut template_types = Vec::new();
730
731        for child_node in all_child_nodes {
732            if let TypeRef::Atomic(inner) = child_node {
733                match inner {
734                    TAtomic::GenericParameter(_) => {
735                        template_types.push(inner);
736                    }
737                    TAtomic::Scalar(TScalar::ClassLikeString(TClassLikeString::Generic { .. })) => {
738                        template_types.push(inner);
739                    }
740                    _ => {}
741                }
742            }
743        }
744
745        template_types
746    }
747
748    pub fn is_objecty(&self) -> bool {
749        for atomic in self.types.as_ref() {
750            if let &TAtomic::Object(_) = atomic {
751                continue;
752            }
753
754            if let TAtomic::Callable(callable) = atomic
755                && callable.get_signature().is_none_or(super::atomic::callable::TCallableSignature::is_closure)
756            {
757                continue;
758            }
759
760            return false;
761        }
762
763        true
764    }
765
766    #[must_use]
767    pub fn is_generator(&self) -> bool {
768        for atomic in self.types.as_ref() {
769            if atomic.is_generator() {
770                continue;
771            }
772
773            return false;
774        }
775
776        true
777    }
778
779    #[must_use]
780    pub fn extends_or_implements(&self, codebase: &CodebaseMetadata, interface: &[u8]) -> bool {
781        for atomic in self.types.as_ref() {
782            if !atomic.extends_or_implements(codebase, interface) {
783                return false;
784            }
785        }
786
787        true
788    }
789
790    #[must_use]
791    pub fn is_generic_parameter(&self) -> bool {
792        self.types.len() == 1 && matches!(self.types[0], TAtomic::GenericParameter(_))
793    }
794
795    #[must_use]
796    pub fn get_generic_parameter_constraint(&self) -> Option<&TUnion> {
797        if self.is_generic_parameter()
798            && let TAtomic::GenericParameter(parameter) = &self.types[0]
799        {
800            return Some(&parameter.constraint);
801        }
802
803        None
804    }
805
806    #[must_use]
807    pub fn is_null(&self) -> bool {
808        self.types.iter().all(|t| matches!(t, TAtomic::Null)) && !self.types.is_empty()
809    }
810
811    #[must_use]
812    pub fn is_nullable(&self) -> bool {
813        self.types.iter().any(|t| match t {
814            TAtomic::Null => self.types.len() >= 2,
815            TAtomic::GenericParameter(param) => param.constraint.is_nullable(),
816            _ => false,
817        })
818    }
819
820    #[must_use]
821    pub fn can_be_null(&self) -> bool {
822        self.types.iter().any(|t| match t {
823            TAtomic::Null => true,
824            TAtomic::Void => true,
825            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
826            TAtomic::GenericParameter(param) => param.constraint.can_be_null(),
827            _ => false,
828        })
829    }
830
831    #[must_use]
832    pub fn is_void(&self) -> bool {
833        self.types.iter().all(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
834    }
835
836    #[must_use]
837    pub fn is_voidable(&self) -> bool {
838        self.types.iter().any(|t| matches!(t, TAtomic::Void)) && !self.types.is_empty()
839    }
840
841    pub fn has_resource(&self) -> bool {
842        self.types.iter().any(TAtomic::is_resource)
843    }
844
845    pub fn is_resource(&self) -> bool {
846        self.types.iter().all(TAtomic::is_resource) && !self.types.is_empty()
847    }
848
849    pub fn is_array(&self) -> bool {
850        self.types.iter().all(TAtomic::is_array) && !self.types.is_empty()
851    }
852
853    pub fn is_list(&self) -> bool {
854        self.types.iter().all(TAtomic::is_list) && !self.types.is_empty()
855    }
856
857    pub fn is_vanilla_array(&self) -> bool {
858        self.types.iter().all(TAtomic::is_vanilla_array) && !self.types.is_empty()
859    }
860
861    pub fn is_keyed_array(&self) -> bool {
862        self.types.iter().all(TAtomic::is_keyed_array) && !self.types.is_empty()
863    }
864
865    pub fn is_falsable(&self) -> bool {
866        self.types.len() >= 2 && self.types.iter().any(TAtomic::is_false)
867    }
868
869    #[must_use]
870    pub fn has_bool(&self) -> bool {
871        self.types.iter().any(|t| t.is_bool() || t.is_generic_scalar()) && !self.types.is_empty()
872    }
873
874    /// Checks if the union explicitly contains the generic `scalar` type.
875    ///
876    /// This is a specific check for the `scalar` type itself, not for a
877    /// combination of types that would form a scalar (e.g., `int|string|bool|float`).
878    /// For that, see `has_scalar_combination`.
879    pub fn has_scalar(&self) -> bool {
880        self.types.iter().any(TAtomic::is_generic_scalar)
881    }
882
883    /// Checks if the union contains a combination of types that is equivalent
884    /// to the generic `scalar` type (i.e., contains `int`, `float`, `bool`, and `string`).
885    #[must_use]
886    pub fn has_scalar_combination(&self) -> bool {
887        const HAS_INT: u8 = 1 << 0;
888        const HAS_FLOAT: u8 = 1 << 1;
889        const HAS_BOOL: u8 = 1 << 2;
890        const HAS_STRING: u8 = 1 << 3;
891        const ALL_SCALARS: u8 = HAS_INT | HAS_FLOAT | HAS_BOOL | HAS_STRING;
892
893        let mut flags = 0u8;
894
895        for atomic in self.types.as_ref() {
896            if atomic.is_int() {
897                flags |= HAS_INT;
898            } else if atomic.is_float() {
899                flags |= HAS_FLOAT;
900            } else if atomic.is_bool() {
901                flags |= HAS_BOOL;
902            } else if atomic.is_string() {
903                flags |= HAS_STRING;
904            } else if atomic.is_array_key() {
905                flags |= HAS_INT | HAS_STRING;
906            } else if atomic.is_numeric() {
907                // We don't add `string` as `numeric-string` does not contain `string` type
908                flags |= HAS_INT | HAS_FLOAT;
909            } else if atomic.is_generic_scalar() {
910                return true;
911            }
912
913            // Early exit if we've already found all scalar types
914            if flags == ALL_SCALARS {
915                return true;
916            }
917        }
918
919        flags == ALL_SCALARS
920    }
921    pub fn has_array_key(&self) -> bool {
922        self.types.iter().any(TAtomic::is_array_key)
923    }
924
925    pub fn has_iterable(&self) -> bool {
926        self.types.iter().any(TAtomic::is_iterable) && !self.types.is_empty()
927    }
928
929    pub fn has_array(&self) -> bool {
930        self.types.iter().any(TAtomic::is_array) && !self.types.is_empty()
931    }
932
933    #[must_use]
934    pub fn has_traversable(&self, codebase: &CodebaseMetadata) -> bool {
935        self.types.iter().any(|atomic| atomic.is_traversable(codebase)) && !self.types.is_empty()
936    }
937
938    #[must_use]
939    pub fn has_array_key_like(&self) -> bool {
940        self.types.iter().any(|atomic| atomic.is_array_key() || atomic.is_int() || atomic.is_string())
941    }
942
943    pub fn has_numeric(&self) -> bool {
944        self.types.iter().any(TAtomic::is_numeric) && !self.types.is_empty()
945    }
946
947    pub fn is_always_truthy(&self) -> bool {
948        self.types.iter().all(TAtomic::is_truthy) && !self.types.is_empty()
949    }
950
951    pub fn is_always_falsy(&self) -> bool {
952        self.types.iter().all(TAtomic::is_falsy) && !self.types.is_empty()
953    }
954
955    #[must_use]
956    pub fn is_literal_of(&self, other: &TUnion) -> bool {
957        let Some(other_atomic_type) = other.types.first() else {
958            return false;
959        };
960
961        match other_atomic_type {
962            TAtomic::Scalar(TScalar::String(_)) => {
963                for self_atomic_type in self.types.as_ref() {
964                    if self_atomic_type.is_string_of_literal_origin() {
965                        continue;
966                    }
967
968                    return false;
969                }
970
971                true
972            }
973            TAtomic::Scalar(TScalar::Integer(_)) => {
974                for self_atomic_type in self.types.as_ref() {
975                    if self_atomic_type.is_literal_int() {
976                        continue;
977                    }
978
979                    return false;
980                }
981
982                true
983            }
984            TAtomic::Scalar(TScalar::Float(_)) => {
985                for self_atomic_type in self.types.as_ref() {
986                    if self_atomic_type.is_literal_float() {
987                        continue;
988                    }
989
990                    return false;
991                }
992
993                true
994            }
995            _ => false,
996        }
997    }
998
999    #[must_use]
1000    pub fn all_literals(&self) -> bool {
1001        self.types
1002            .iter()
1003            .all(|atomic| atomic.is_string_of_literal_origin() || atomic.is_literal_int() || atomic.is_literal_float())
1004    }
1005
1006    #[must_use]
1007    pub fn has_static_object(&self) -> bool {
1008        self.types
1009            .iter()
1010            .any(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_static))
1011    }
1012
1013    #[must_use]
1014    pub fn is_static_object(&self) -> bool {
1015        self.types
1016            .iter()
1017            .all(|atomic| matches!(atomic, TAtomic::Object(TObject::Named(named_object)) if named_object.is_static))
1018    }
1019
1020    #[inline]
1021    #[must_use]
1022    pub fn is_single(&self) -> bool {
1023        self.types.len() == 1
1024    }
1025
1026    #[inline]
1027    #[must_use]
1028    pub fn get_single_string(&self) -> Option<&TString> {
1029        if self.is_single()
1030            && let TAtomic::Scalar(TScalar::String(string)) = &self.types[0]
1031        {
1032            Some(string)
1033        } else {
1034            None
1035        }
1036    }
1037
1038    #[inline]
1039    #[must_use]
1040    pub fn get_single_array(&self) -> Option<&TArray> {
1041        if self.is_single()
1042            && let TAtomic::Array(array) = &self.types[0]
1043        {
1044            Some(array)
1045        } else {
1046            None
1047        }
1048    }
1049
1050    #[inline]
1051    #[must_use]
1052    pub fn get_single_bool(&self) -> Option<&TBool> {
1053        if self.is_single()
1054            && let TAtomic::Scalar(TScalar::Bool(bool)) = &self.types[0]
1055        {
1056            Some(bool)
1057        } else {
1058            None
1059        }
1060    }
1061
1062    #[inline]
1063    #[must_use]
1064    pub fn get_single_named_object(&self) -> Option<&TNamedObject> {
1065        if self.is_single()
1066            && let TAtomic::Object(TObject::Named(named_object)) = &self.types[0]
1067        {
1068            Some(named_object)
1069        } else {
1070            None
1071        }
1072    }
1073
1074    #[inline]
1075    #[must_use]
1076    pub fn get_single_shaped_object(&self) -> Option<&TObjectWithProperties> {
1077        if self.is_single()
1078            && let TAtomic::Object(TObject::WithProperties(shaped_object)) = &self.types[0]
1079        {
1080            Some(shaped_object)
1081        } else {
1082            None
1083        }
1084    }
1085
1086    #[inline]
1087    #[must_use]
1088    pub fn get_single(&self) -> &TAtomic {
1089        &self.types[0]
1090    }
1091
1092    #[inline]
1093    #[must_use]
1094    pub fn get_single_owned(self) -> TAtomic {
1095        self.types[0].clone()
1096    }
1097
1098    #[inline]
1099    #[must_use]
1100    pub fn is_named_object(&self) -> bool {
1101        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1102    }
1103
1104    #[must_use]
1105    pub fn is_enum(&self) -> bool {
1106        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(_))))
1107    }
1108
1109    #[must_use]
1110    pub fn is_enum_case(&self) -> bool {
1111        self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1112    }
1113
1114    #[must_use]
1115    pub fn is_single_enum_case(&self) -> bool {
1116        self.is_single()
1117            && self.types.iter().all(|t| matches!(t, TAtomic::Object(TObject::Enum(r#enum)) if r#enum.case.is_some()))
1118    }
1119
1120    #[inline]
1121    #[must_use]
1122    pub fn has_named_object(&self) -> bool {
1123        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Named(_))))
1124    }
1125
1126    #[inline]
1127    #[must_use]
1128    pub fn has_object(&self) -> bool {
1129        self.types.iter().any(|t| matches!(t, TAtomic::Object(TObject::Any | TObject::WithProperties(_))))
1130    }
1131
1132    #[inline]
1133    #[must_use]
1134    pub fn has_callable(&self) -> bool {
1135        self.types.iter().any(|t| matches!(t, TAtomic::Callable(_)))
1136    }
1137
1138    #[inline]
1139    #[must_use]
1140    pub fn is_callable(&self) -> bool {
1141        self.types.iter().all(|t| matches!(t, TAtomic::Callable(_)))
1142    }
1143
1144    #[inline]
1145    #[must_use]
1146    pub fn has_object_type(&self) -> bool {
1147        self.types.iter().any(|t| matches!(t, TAtomic::Object(_)))
1148    }
1149
1150    /// Return a vector of pairs containing the enum name, and their case name
1151    /// if specified.
1152    #[must_use]
1153    pub fn get_enum_cases(&self) -> Vec<(Word, Option<Word>)> {
1154        self.types
1155            .iter()
1156            .filter_map(|t| match t {
1157                TAtomic::Object(TObject::Enum(enum_object)) => Some((enum_object.name, enum_object.case)),
1158                _ => None,
1159            })
1160            .collect()
1161    }
1162
1163    #[must_use]
1164    pub fn get_single_int(&self) -> Option<TInteger> {
1165        if self.is_single() { self.get_single().get_integer() } else { None }
1166    }
1167
1168    #[must_use]
1169    pub fn get_single_literal_int_value(&self) -> Option<i64> {
1170        if self.is_single() { self.get_single().get_literal_int_value() } else { None }
1171    }
1172
1173    #[must_use]
1174    pub fn get_single_maximum_int_value(&self) -> Option<i64> {
1175        if self.is_single() { self.get_single().get_maximum_int_value() } else { None }
1176    }
1177
1178    #[must_use]
1179    pub fn get_single_minimum_int_value(&self) -> Option<i64> {
1180        if self.is_single() { self.get_single().get_minimum_int_value() } else { None }
1181    }
1182
1183    #[must_use]
1184    pub fn get_single_literal_float_value(&self) -> Option<f64> {
1185        if self.is_single() { self.get_single().get_literal_float_value() } else { None }
1186    }
1187
1188    #[must_use]
1189    pub fn get_single_literal_string_value(&self) -> Option<&[u8]> {
1190        if self.is_single() { self.get_single().get_literal_string_value() } else { None }
1191    }
1192
1193    #[must_use]
1194    pub fn get_single_class_string_value(&self) -> Option<Word> {
1195        if self.is_single() { self.get_single().get_class_string_value() } else { None }
1196    }
1197
1198    #[must_use]
1199    pub fn get_single_array_key(&self) -> Option<ArrayKey> {
1200        if self.is_single() { self.get_single().to_array_key() } else { None }
1201    }
1202
1203    #[must_use]
1204    pub fn get_single_key_of_array_like(&self) -> Option<TUnion> {
1205        if !self.is_single() {
1206            return None;
1207        }
1208
1209        match self.get_single() {
1210            TAtomic::Array(array) => match array {
1211                TArray::List(_) => Some(get_int()),
1212                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1213                    Some((k, _)) => Some((**k).clone()),
1214                    None => Some(get_arraykey()),
1215                },
1216            },
1217            _ => None,
1218        }
1219    }
1220
1221    #[must_use]
1222    pub fn get_single_value_of_array_like(&self) -> Option<Cow<'_, TUnion>> {
1223        if !self.is_single() {
1224            return None;
1225        }
1226
1227        match self.get_single() {
1228            TAtomic::Array(array) => match array {
1229                TArray::List(list) => Some(Cow::Borrowed(&list.element_type)),
1230                TArray::Keyed(keyed_array) => match &keyed_array.parameters {
1231                    Some((_, v)) => Some(Cow::Borrowed(v)),
1232                    None => Some(Cow::Owned(get_mixed())),
1233                },
1234            },
1235            _ => None,
1236        }
1237    }
1238
1239    #[must_use]
1240    pub fn get_literal_ints(&self) -> Vec<&TAtomic> {
1241        self.types.iter().filter(|a| a.is_literal_int()).collect()
1242    }
1243
1244    #[must_use]
1245    pub fn get_literal_strings(&self) -> Vec<&TAtomic> {
1246        self.types.iter().filter(|a| a.is_known_literal_string()).collect()
1247    }
1248
1249    #[must_use]
1250    pub fn get_literal_string_values(&self) -> Vec<Option<Word>> {
1251        self.get_literal_strings()
1252            .into_iter()
1253            .map(|atom| match atom {
1254                TAtomic::Scalar(TScalar::String(TString { literal: Some(TStringLiteral::Value(value)), .. })) => {
1255                    Some(*value)
1256                }
1257                _ => None,
1258            })
1259            .collect()
1260    }
1261
1262    #[must_use]
1263    pub fn has_literal_float(&self) -> bool {
1264        self.types.iter().any(|atomic| match atomic {
1265            TAtomic::Scalar(scalar) => scalar.is_literal_float(),
1266            _ => false,
1267        })
1268    }
1269
1270    #[must_use]
1271    pub fn has_literal_int(&self) -> bool {
1272        self.types.iter().any(|atomic| match atomic {
1273            TAtomic::Scalar(scalar) => scalar.is_literal_int(),
1274            _ => false,
1275        })
1276    }
1277
1278    #[must_use]
1279    pub fn has_literal_string(&self) -> bool {
1280        self.types.iter().any(|atomic| match atomic {
1281            TAtomic::Scalar(scalar) => scalar.is_known_literal_string(),
1282            _ => false,
1283        })
1284    }
1285
1286    #[must_use]
1287    pub fn has_literal_value(&self) -> bool {
1288        self.types.iter().any(|atomic| match atomic {
1289            TAtomic::Scalar(scalar) => scalar.is_literal_value(),
1290            _ => false,
1291        })
1292    }
1293
1294    #[must_use]
1295    pub fn accepts_false(&self) -> bool {
1296        self.types.iter().any(|t| match t {
1297            TAtomic::GenericParameter(parameter) => parameter.constraint.accepts_false(),
1298            TAtomic::Mixed(mixed) if !mixed.is_truthy() => true,
1299            TAtomic::Scalar(TScalar::Generic | TScalar::Bool(TBool { value: None | Some(false) })) => true,
1300            _ => false,
1301        })
1302    }
1303
1304    #[must_use]
1305    pub fn accepts_null(&self) -> bool {
1306        self.types.iter().any(|t| match t {
1307            TAtomic::GenericParameter(generic_parameter) => generic_parameter.constraint.accepts_null(),
1308            TAtomic::Mixed(mixed) if !mixed.is_non_null() => true,
1309            TAtomic::Null | TAtomic::Placeholder => true,
1310            _ => false,
1311        })
1312    }
1313}
1314
1315impl TType for TUnion {
1316    fn get_child_nodes(&self) -> Vec<TypeRef<'_>> {
1317        self.types.iter().map(TypeRef::Atomic).collect()
1318    }
1319
1320    fn needs_population(&self) -> bool {
1321        !self.flags.contains(UnionFlags::POPULATED) && self.types.iter().any(super::TType::needs_population)
1322    }
1323
1324    #[inline]
1325    fn is_expandable(&self) -> bool {
1326        if self.types.is_empty() {
1327            return true;
1328        }
1329
1330        self.types.iter().any(super::TType::is_expandable)
1331    }
1332
1333    fn is_complex(&self) -> bool {
1334        self.types.len() > 3 || self.types.iter().any(super::TType::is_complex)
1335    }
1336
1337    fn get_id(&self) -> Word {
1338        let len = self.types.len();
1339
1340        let mut atomic_ids: Vec<Word> = self
1341            .types
1342            .as_ref()
1343            .iter()
1344            .map(|atomic| {
1345                let id = atomic.get_id();
1346                if atomic.is_generic_parameter() || atomic.has_intersection_types() && len > 1 {
1347                    concat_word!(b"(", id.as_bytes(), b")")
1348                } else {
1349                    id
1350                }
1351            })
1352            .collect();
1353
1354        if len <= 1 {
1355            return atomic_ids.pop().unwrap_or_else(empty_word);
1356        }
1357
1358        atomic_ids.sort_unstable();
1359        let mut result = atomic_ids[0];
1360        for id in &atomic_ids[1..] {
1361            result = concat_word!(result.as_bytes(), b"|", id.as_bytes());
1362        }
1363
1364        result
1365    }
1366
1367    fn get_pretty_id_with_indent(&self, indent: usize) -> Word {
1368        let len = self.types.len();
1369
1370        if len <= 1 {
1371            return self.types.first().map_or_else(empty_word, |atomic| atomic.get_pretty_id_with_indent(indent));
1372        }
1373
1374        // Use multiline format for unions with more than 3 types
1375        if len > 3 {
1376            let mut atomic_ids: Vec<Word> = self
1377                .types
1378                .as_ref()
1379                .iter()
1380                .map(|atomic| {
1381                    let id = atomic.get_pretty_id_with_indent(indent + 2);
1382                    if atomic.has_intersection_types() { concat_word!(b"(", id.as_bytes(), b")") } else { id }
1383                })
1384                .collect();
1385
1386            atomic_ids.sort_unstable();
1387
1388            let mut result: Vec<u8> = Vec::new();
1389            result.extend_from_slice(atomic_ids[0].as_bytes());
1390            for id in &atomic_ids[1..] {
1391                result.extend_from_slice(b"\n");
1392                result.resize(result.len() + indent, b' ');
1393                result.extend_from_slice(b"| ");
1394                result.extend_from_slice(id.as_bytes());
1395            }
1396
1397            word(&result)
1398        } else {
1399            // Use inline format for smaller unions
1400            let mut atomic_ids: Vec<Word> = self
1401                .types
1402                .as_ref()
1403                .iter()
1404                .map(|atomic| {
1405                    let id = atomic.get_pretty_id_with_indent(indent);
1406                    if atomic.has_intersection_types() && len > 1 {
1407                        concat_word!(b"(", id.as_bytes(), b")")
1408                    } else {
1409                        id
1410                    }
1411                })
1412                .collect();
1413
1414            atomic_ids.sort_unstable();
1415            let mut result = atomic_ids[0];
1416            for id in &atomic_ids[1..] {
1417                result = concat_word!(result.as_bytes(), b" | ", id.as_bytes());
1418            }
1419
1420            result
1421        }
1422    }
1423}
1424
1425impl PartialEq for TUnion {
1426    fn eq(&self, other: &TUnion) -> bool {
1427        if std::ptr::eq(self, other) {
1428            return true;
1429        }
1430
1431        const SEMANTIC_FLAGS: UnionFlags = UnionFlags::HAD_TEMPLATE
1432            .union(UnionFlags::BY_REFERENCE)
1433            .union(UnionFlags::REFERENCE_FREE)
1434            .union(UnionFlags::POSSIBLY_UNDEFINED_FROM_TRY)
1435            .union(UnionFlags::POSSIBLY_UNDEFINED)
1436            .union(UnionFlags::IGNORE_NULLABLE_ISSUES)
1437            .union(UnionFlags::IGNORE_FALSABLE_ISSUES)
1438            .union(UnionFlags::FROM_TEMPLATE_DEFAULT);
1439
1440        if self.flags.intersection(SEMANTIC_FLAGS) != other.flags.intersection(SEMANTIC_FLAGS) {
1441            return false;
1442        }
1443
1444        let len = self.types.len();
1445        if len != other.types.len() {
1446            return false;
1447        }
1448
1449        // Fast path: unions are commonly constructed in stable type order.
1450        // When order already matches, this keeps comparison linear.
1451        if self.types == other.types {
1452            return true;
1453        }
1454
1455        // Check self ⊆ other
1456        for i in 0..len {
1457            let mut has_match = false;
1458            for j in 0..len {
1459                if self.types[i] == other.types[j] {
1460                    has_match = true;
1461                    break;
1462                }
1463            }
1464
1465            if !has_match {
1466                return false;
1467            }
1468        }
1469
1470        // Check other ⊆ self (needed when duplicates exist in either side)
1471        for i in 0..len {
1472            let mut has_match = false;
1473            for j in 0..len {
1474                if other.types[i] == self.types[j] {
1475                    has_match = true;
1476                    break;
1477                }
1478            }
1479
1480            if !has_match {
1481                return false;
1482            }
1483        }
1484
1485        true
1486    }
1487}
1488
1489/// Coerces an atomic Vec to be non-empty by inserting `Never` when empty.
1490#[inline]
1491fn ensure_non_empty_types(mut types: Vec<TAtomic>) -> Vec<TAtomic> {
1492    if types.is_empty() {
1493        types.push(TAtomic::Never);
1494    }
1495
1496    types
1497}
1498
1499pub fn populate_union_type(
1500    unpopulated_union: &mut TUnion,
1501    codebase_symbols: &Symbols,
1502    reference_source: Option<&ReferenceSource>,
1503    symbol_references: &mut SymbolReferences,
1504    force: bool,
1505) {
1506    if unpopulated_union.flags.contains(UnionFlags::POPULATED) && !force {
1507        return;
1508    }
1509
1510    if !unpopulated_union.needs_population() {
1511        return;
1512    }
1513
1514    unpopulated_union.flags.insert(UnionFlags::POPULATED);
1515    let unpopulated_atomics = unpopulated_union.types.to_mut();
1516    for unpopulated_atomic in unpopulated_atomics {
1517        match unpopulated_atomic {
1518            TAtomic::Scalar(TScalar::ClassLikeString(
1519                TClassLikeString::Generic { constraint, .. } | TClassLikeString::OfType { constraint, .. },
1520            )) => {
1521                populate_atomic_type(
1522                    Arc::make_mut(constraint),
1523                    codebase_symbols,
1524                    reference_source,
1525                    symbol_references,
1526                    force,
1527                );
1528            }
1529            _ => {
1530                populate_atomic_type(unpopulated_atomic, codebase_symbols, reference_source, symbol_references, force);
1531            }
1532        }
1533    }
1534}
1535
1536/// Recursively generalises every narrowed scalar (string, int, float, bool) to
1537/// its general form within the given atomic type.  Descends into every
1538/// nested `TUnion` so a narrowing buried in an array element, object type
1539/// parameter, generic constraint, conditional branch, etc., is also widened.
1540fn widen_atomic_scalars(atomic: &mut TAtomic) {
1541    match atomic {
1542        TAtomic::Scalar(scalar) => widen_scalar(scalar),
1543        TAtomic::Array(array) => match array {
1544            TArray::List(list) => {
1545                widen_arc_union_scalars(&mut list.element_type);
1546                if let Some(known) = list.known_elements.as_mut() {
1547                    for (_, ty) in known.values_mut() {
1548                        ty.widen_scalars();
1549                    }
1550                }
1551            }
1552            TArray::Keyed(keyed) => {
1553                if let Some((key, value)) = keyed.parameters.as_mut() {
1554                    widen_arc_union_scalars(key);
1555                    widen_arc_union_scalars(value);
1556                }
1557                if let Some(known) = keyed.known_items.as_mut() {
1558                    for (_, ty) in known.values_mut() {
1559                        ty.widen_scalars();
1560                    }
1561                }
1562            }
1563        },
1564        TAtomic::Iterable(iterable) => {
1565            widen_arc_union_scalars(&mut iterable.key_type);
1566            widen_arc_union_scalars(&mut iterable.value_type);
1567            if let Some(intersections) = iterable.intersection_types.as_mut() {
1568                for inner in intersections.iter_mut() {
1569                    widen_atomic_scalars(inner);
1570                }
1571            }
1572        }
1573        TAtomic::Object(TObject::Named(named)) => {
1574            if let Some(params) = named.type_parameters.as_mut() {
1575                for ty in params.iter_mut() {
1576                    ty.widen_scalars();
1577                }
1578            }
1579        }
1580        TAtomic::Object(TObject::WithProperties(with_props)) => {
1581            for (_, ty) in with_props.known_properties.values_mut() {
1582                ty.widen_scalars();
1583            }
1584        }
1585        TAtomic::GenericParameter(generic) => {
1586            widen_arc_union_scalars(&mut generic.constraint);
1587            if let Some(intersections) = generic.intersection_types.as_mut() {
1588                for inner in intersections.iter_mut() {
1589                    widen_atomic_scalars(inner);
1590                }
1591            }
1592        }
1593        TAtomic::Conditional(conditional) => {
1594            widen_arc_union_scalars(&mut conditional.subject);
1595            widen_arc_union_scalars(&mut conditional.target);
1596            widen_arc_union_scalars(&mut conditional.then);
1597            widen_arc_union_scalars(&mut conditional.otherwise);
1598        }
1599        _ => {}
1600    }
1601}
1602
1603#[inline]
1604fn widen_arc_union_scalars(union: &mut Arc<TUnion>) {
1605    if union_has_widenable_nested_scalar(union) {
1606        Arc::make_mut(union).widen_scalars();
1607    }
1608}
1609
1610fn widen_scalar(scalar: &mut TScalar) {
1611    match scalar {
1612        TScalar::String(string) if !is_string_fully_general(string) => {
1613            *string = TString::general();
1614        }
1615        TScalar::Integer(integer) if !matches!(integer, TInteger::Unspecified) => {
1616            *integer = TInteger::Unspecified;
1617        }
1618        TScalar::Float(float) if !matches!(float, TFloat::Float) => {
1619            *float = TFloat::Float;
1620        }
1621        TScalar::Bool(b) if !b.is_general() => {
1622            *b = TBool::general();
1623        }
1624        _ => {}
1625    }
1626}
1627
1628#[inline]
1629fn is_string_fully_general(string: &TString) -> bool {
1630    string.literal.is_none()
1631        && !string.is_numeric
1632        && !string.is_truthy
1633        && !string.is_non_empty
1634        && !string.is_callable
1635        && matches!(string.casing, TStringCasing::Unspecified)
1636}
1637
1638/// Like `widen_atomic_scalars`, but only widens *literal* scalars (e.g.
1639/// `int(42)`, `'foo'`, `true`/`false`) - preserves user-declared narrowings
1640/// such as `non-negative-int`, `non-empty-string`, or `int<1, max>`. Used when
1641/// the surrounding context (e.g. `@param-out` on a generic function) commits
1642/// to maintaining narrow types through the call.
1643fn widen_atomic_literals(atomic: &mut TAtomic) {
1644    match atomic {
1645        TAtomic::Scalar(scalar) => widen_scalar_literal(scalar),
1646        TAtomic::Array(array) => match array {
1647            TArray::List(list) => {
1648                widen_arc_union_literals(&mut list.element_type);
1649                if let Some(known) = list.known_elements.as_mut() {
1650                    for (_, ty) in known.values_mut() {
1651                        ty.widen_literals();
1652                    }
1653                }
1654            }
1655            TArray::Keyed(keyed) => {
1656                if let Some((key, value)) = keyed.parameters.as_mut() {
1657                    widen_arc_union_literals(key);
1658                    widen_arc_union_literals(value);
1659                }
1660                if let Some(known) = keyed.known_items.as_mut() {
1661                    for (_, ty) in known.values_mut() {
1662                        ty.widen_literals();
1663                    }
1664                }
1665            }
1666        },
1667        TAtomic::Iterable(iterable) => {
1668            widen_arc_union_literals(&mut iterable.key_type);
1669            widen_arc_union_literals(&mut iterable.value_type);
1670            if let Some(intersections) = iterable.intersection_types.as_mut() {
1671                for inner in intersections.iter_mut() {
1672                    widen_atomic_literals(inner);
1673                }
1674            }
1675        }
1676        TAtomic::Object(TObject::Named(named)) => {
1677            if let Some(params) = named.type_parameters.as_mut() {
1678                for ty in params.iter_mut() {
1679                    ty.widen_literals();
1680                }
1681            }
1682        }
1683        TAtomic::Object(TObject::WithProperties(with_props)) => {
1684            for (_, ty) in with_props.known_properties.values_mut() {
1685                ty.widen_literals();
1686            }
1687        }
1688        TAtomic::GenericParameter(generic) => {
1689            widen_arc_union_literals(&mut generic.constraint);
1690            if let Some(intersections) = generic.intersection_types.as_mut() {
1691                for inner in intersections.iter_mut() {
1692                    widen_atomic_literals(inner);
1693                }
1694            }
1695        }
1696        TAtomic::Conditional(conditional) => {
1697            widen_arc_union_literals(&mut conditional.subject);
1698            widen_arc_union_literals(&mut conditional.target);
1699            widen_arc_union_literals(&mut conditional.then);
1700            widen_arc_union_literals(&mut conditional.otherwise);
1701        }
1702        _ => {}
1703    }
1704}
1705
1706#[inline]
1707fn widen_arc_union_literals(union: &mut Arc<TUnion>) {
1708    if union_has_widenable_nested_literal(union) {
1709        Arc::make_mut(union).widen_literals();
1710    }
1711}
1712
1713fn widen_scalar_literal(scalar: &mut TScalar) {
1714    match scalar {
1715        TScalar::String(string) if string.literal.is_some() => {
1716            *string = string.without_literal();
1717        }
1718        TScalar::Integer(integer) if matches!(integer, TInteger::Literal(_) | TInteger::UnspecifiedLiteral) => {
1719            *integer = TInteger::Unspecified;
1720        }
1721        TScalar::Float(float) if matches!(float, TFloat::Literal(_) | TFloat::UnspecifiedLiteral) => {
1722            *float = TFloat::Float;
1723        }
1724        TScalar::Bool(b) if !b.is_general() => {
1725            *b = TBool::general();
1726        }
1727        _ => {}
1728    }
1729}
1730
1731fn union_has_widenable_nested_literal(union: &TUnion) -> bool {
1732    union.types.iter().any(atomic_has_widenable_literal)
1733}
1734
1735fn atomic_has_widenable_literal(atomic: &TAtomic) -> bool {
1736    match atomic {
1737        TAtomic::Scalar(TScalar::String(s)) => s.literal.is_some(),
1738        TAtomic::Scalar(TScalar::Integer(i)) => matches!(i, TInteger::Literal(_) | TInteger::UnspecifiedLiteral),
1739        TAtomic::Scalar(TScalar::Float(f)) => matches!(f, TFloat::Literal(_) | TFloat::UnspecifiedLiteral),
1740        TAtomic::Scalar(TScalar::Bool(b)) => !b.is_general(),
1741        TAtomic::Array(TArray::List(list)) => {
1742            union_has_widenable_nested_literal(&list.element_type)
1743                || list
1744                    .known_elements
1745                    .as_ref()
1746                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_literal(t)))
1747        }
1748        TAtomic::Array(TArray::Keyed(keyed)) => {
1749            keyed
1750                .parameters
1751                .as_ref()
1752                .is_some_and(|(k, v)| union_has_widenable_nested_literal(k) || union_has_widenable_nested_literal(v))
1753                || keyed
1754                    .known_items
1755                    .as_ref()
1756                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_literal(t)))
1757        }
1758        TAtomic::Iterable(iterable) => {
1759            union_has_widenable_nested_literal(&iterable.key_type)
1760                || union_has_widenable_nested_literal(&iterable.value_type)
1761                || iterable.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_literal))
1762        }
1763        TAtomic::Object(TObject::Named(named)) => {
1764            named.type_parameters.as_ref().is_some_and(|p| p.iter().any(union_has_widenable_nested_literal))
1765        }
1766        TAtomic::Object(TObject::WithProperties(with_props)) => {
1767            with_props.known_properties.values().any(|(_, t)| union_has_widenable_nested_literal(t))
1768        }
1769        TAtomic::GenericParameter(generic) => {
1770            union_has_widenable_nested_literal(&generic.constraint)
1771                || generic.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_literal))
1772        }
1773        TAtomic::Conditional(conditional) => {
1774            union_has_widenable_nested_literal(&conditional.subject)
1775                || union_has_widenable_nested_literal(&conditional.target)
1776                || union_has_widenable_nested_literal(&conditional.then)
1777                || union_has_widenable_nested_literal(&conditional.otherwise)
1778        }
1779        _ => false,
1780    }
1781}
1782
1783/// Returns `true` if any atom in the union holds (somewhere recursively) a
1784/// type that `widen_atomic_scalars` would mutate.  Used as a cheap pre-check
1785/// to avoid `Arc::make_mut` on shared unions that don't need widening.
1786fn union_has_widenable_nested_scalar(union: &TUnion) -> bool {
1787    union.types.iter().any(atomic_has_widenable_scalar)
1788}
1789
1790fn atomic_has_widenable_scalar(atomic: &TAtomic) -> bool {
1791    match atomic {
1792        TAtomic::Scalar(TScalar::String(s)) => !is_string_fully_general(s),
1793        TAtomic::Scalar(TScalar::Integer(i)) => !matches!(i, TInteger::Unspecified),
1794        TAtomic::Scalar(TScalar::Float(f)) => !matches!(f, TFloat::Float),
1795        TAtomic::Scalar(TScalar::Bool(b)) => !b.is_general(),
1796        TAtomic::Array(TArray::List(list)) => {
1797            union_has_widenable_nested_scalar(&list.element_type)
1798                || list
1799                    .known_elements
1800                    .as_ref()
1801                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_scalar(t)))
1802        }
1803        TAtomic::Array(TArray::Keyed(keyed)) => {
1804            keyed
1805                .parameters
1806                .as_ref()
1807                .is_some_and(|(k, v)| union_has_widenable_nested_scalar(k) || union_has_widenable_nested_scalar(v))
1808                || keyed
1809                    .known_items
1810                    .as_ref()
1811                    .is_some_and(|m| m.values().any(|(_, t)| union_has_widenable_nested_scalar(t)))
1812        }
1813        TAtomic::Iterable(iterable) => {
1814            union_has_widenable_nested_scalar(&iterable.key_type)
1815                || union_has_widenable_nested_scalar(&iterable.value_type)
1816                || iterable.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_scalar))
1817        }
1818        TAtomic::Object(TObject::Named(named)) => {
1819            named.type_parameters.as_ref().is_some_and(|p| p.iter().any(union_has_widenable_nested_scalar))
1820        }
1821        TAtomic::Object(TObject::WithProperties(with_props)) => {
1822            with_props.known_properties.values().any(|(_, t)| union_has_widenable_nested_scalar(t))
1823        }
1824        TAtomic::GenericParameter(generic) => {
1825            union_has_widenable_nested_scalar(&generic.constraint)
1826                || generic.intersection_types.as_ref().is_some_and(|v| v.iter().any(atomic_has_widenable_scalar))
1827        }
1828        TAtomic::Conditional(conditional) => {
1829            union_has_widenable_nested_scalar(&conditional.subject)
1830                || union_has_widenable_nested_scalar(&conditional.target)
1831                || union_has_widenable_nested_scalar(&conditional.then)
1832                || union_has_widenable_nested_scalar(&conditional.otherwise)
1833        }
1834        _ => false,
1835    }
1836}