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