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