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