1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4use smallvec::SmallVec;
5
6use crate::atomic::Atomic;
7
8pub type AtomicVec = SmallVec<[Atomic; 2]>;
10
11#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
16pub struct Union {
17 pub types: AtomicVec,
18 pub possibly_undefined: bool,
20 pub from_docblock: bool,
22}
23
24impl Union {
25 pub fn empty() -> Self {
28 Self {
29 types: SmallVec::new(),
30 possibly_undefined: false,
31 from_docblock: false,
32 }
33 }
34
35 pub fn single(atomic: Atomic) -> Self {
36 let mut types = SmallVec::new();
37 types.push(atomic);
38 Self {
39 types,
40 possibly_undefined: false,
41 from_docblock: false,
42 }
43 }
44
45 pub fn mixed() -> Self {
46 Self::single(Atomic::TMixed)
47 }
48
49 pub fn void() -> Self {
50 Self::single(Atomic::TVoid)
51 }
52
53 pub fn never() -> Self {
54 Self::single(Atomic::TNever)
55 }
56
57 pub fn null() -> Self {
58 Self::single(Atomic::TNull)
59 }
60
61 pub fn bool() -> Self {
62 Self::single(Atomic::TBool)
63 }
64
65 pub fn int() -> Self {
66 Self::single(Atomic::TInt)
67 }
68
69 pub fn float() -> Self {
70 Self::single(Atomic::TFloat)
71 }
72
73 pub fn string() -> Self {
74 Self::single(Atomic::TString)
75 }
76
77 pub fn nullable(atomic: Atomic) -> Self {
79 let mut types = SmallVec::new();
80 types.push(atomic);
81 types.push(Atomic::TNull);
82 Self {
83 types,
84 possibly_undefined: false,
85 from_docblock: false,
86 }
87 }
88
89 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
91 let mut u = Self::empty();
92 for a in atomics {
93 u.add_type(a);
94 }
95 u
96 }
97
98 pub fn is_empty(&self) -> bool {
101 self.types.is_empty()
102 }
103
104 pub fn is_single(&self) -> bool {
105 self.types.len() == 1
106 }
107
108 pub fn is_nullable(&self) -> bool {
109 self.types.iter().any(|t| matches!(t, Atomic::TNull))
110 }
111
112 pub fn is_mixed(&self) -> bool {
113 self.types.iter().any(|t| match t {
114 Atomic::TMixed => true,
115 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
116 _ => false,
117 })
118 }
119
120 pub fn is_never(&self) -> bool {
121 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
122 }
123
124 pub fn is_void(&self) -> bool {
125 self.is_single() && matches!(self.types[0], Atomic::TVoid)
126 }
127
128 pub fn can_be_falsy(&self) -> bool {
129 self.types.iter().any(|t| t.can_be_falsy())
130 }
131
132 pub fn can_be_truthy(&self) -> bool {
133 self.types.iter().any(|t| t.can_be_truthy())
134 }
135
136 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
137 self.types.iter().any(f)
138 }
139
140 pub fn has_named_object(&self, fqcn: &str) -> bool {
141 self.types.iter().any(|t| match t {
142 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
143 _ => false,
144 })
145 }
146
147 pub fn add_type(&mut self, atomic: Atomic) {
152 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
154 return;
155 }
156
157 if matches!(atomic, Atomic::TMixed) {
159 self.types.clear();
160 self.types.push(Atomic::TMixed);
161 return;
162 }
163
164 if self.types.contains(&atomic) {
166 return;
167 }
168
169 if let Atomic::TLiteralInt(_) = &atomic {
171 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
172 return;
173 }
174 }
175 if let Atomic::TLiteralString(_) = &atomic {
177 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
178 return;
179 }
180 }
181 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
183 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
184 {
185 return;
186 }
187 if matches!(atomic, Atomic::TInt) {
189 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
190 }
191 if matches!(atomic, Atomic::TString) {
193 self.types
194 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
195 }
196 if matches!(atomic, Atomic::TBool) {
198 self.types
199 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
200 }
201
202 self.types.push(atomic);
203 }
204
205 pub fn remove_null(&self) -> Union {
209 self.filter(|t| !matches!(t, Atomic::TNull))
210 }
211
212 pub fn remove_false(&self) -> Union {
214 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
215 }
216
217 pub fn core_type(&self) -> Union {
219 self.remove_null().remove_false()
220 }
221
222 pub fn narrow_to_truthy(&self) -> Union {
224 if self.is_mixed() {
225 return Union::mixed();
226 }
227 let narrowed = self.filter(|t| t.can_be_truthy());
228 narrowed.filter(|t| match t {
230 Atomic::TLiteralInt(0) => false,
231 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
232 Atomic::TLiteralFloat(0, 0) => false,
233 _ => true,
234 })
235 }
236
237 pub fn narrow_to_falsy(&self) -> Union {
239 if self.is_mixed() {
240 return Union::from_vec(vec![
241 Atomic::TNull,
242 Atomic::TFalse,
243 Atomic::TLiteralInt(0),
244 Atomic::TLiteralString("".into()),
245 ]);
246 }
247 self.filter(|t| t.can_be_falsy())
248 }
249
250 pub fn narrow_instanceof(&self, class: &str) -> Union {
256 let narrowed_ty = Atomic::TNamedObject {
257 fqcn: class.into(),
258 type_params: vec![],
259 };
260 let has_object = self.types.iter().any(|t| {
262 matches!(
263 t,
264 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
266 });
267 if has_object || self.is_empty() {
268 Union::single(narrowed_ty)
269 } else {
270 Union::single(narrowed_ty)
273 }
274 }
275
276 pub fn narrow_to_string(&self) -> Union {
278 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
279 }
280
281 pub fn narrow_to_int(&self) -> Union {
283 self.filter(|t| {
284 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
285 })
286 }
287
288 pub fn narrow_to_float(&self) -> Union {
290 self.filter(|t| {
291 matches!(
292 t,
293 Atomic::TFloat
294 | Atomic::TLiteralFloat(..)
295 | Atomic::TMixed
296 | Atomic::TScalar
297 | Atomic::TNumeric
298 )
299 })
300 }
301
302 pub fn narrow_to_bool(&self) -> Union {
304 self.filter(|t| {
305 matches!(
306 t,
307 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
308 )
309 })
310 }
311
312 pub fn narrow_to_null(&self) -> Union {
314 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
315 }
316
317 pub fn narrow_to_array(&self) -> Union {
319 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
320 }
321
322 pub fn narrow_to_object(&self) -> Union {
324 self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
325 }
326
327 pub fn narrow_to_callable(&self) -> Union {
329 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
330 }
331
332 pub fn narrow_to_scalar(&self) -> Union {
334 self.filter(|t| {
335 matches!(
336 t,
337 Atomic::TString
338 | Atomic::TLiteralString(..)
339 | Atomic::TNumericString
340 | Atomic::TInt
341 | Atomic::TLiteralInt(..)
342 | Atomic::TFloat
343 | Atomic::TLiteralFloat(..)
344 | Atomic::TBool
345 | Atomic::TTrue
346 | Atomic::TFalse
347 | Atomic::TScalar
348 | Atomic::TMixed
349 )
350 })
351 }
352
353 pub fn narrow_to_iterable(&self) -> Union {
356 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
357 }
358
359 pub fn narrow_to_countable(&self) -> Union {
362 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
363 }
364
365 pub fn narrow_to_resource(&self) -> Union {
369 self.filter(|t| matches!(t, Atomic::TMixed))
371 }
372
373 pub fn merge(a: &Union, b: &Union) -> Union {
378 let mut result = a.clone();
379 for atomic in &b.types {
380 result.add_type(atomic.clone());
381 }
382 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
383 result
384 }
385
386 pub fn intersect_with(&self, other: &Union) -> Union {
390 if self.is_mixed() {
391 return other.clone();
392 }
393 if other.is_mixed() {
394 return self.clone();
395 }
396 let mut result = Union::empty();
398 for a in &self.types {
399 for b in &other.types {
400 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
401 result.add_type(a.clone());
402 break;
403 }
404 }
405 }
406 if result.is_empty() {
408 other.clone()
409 } else {
410 result
411 }
412 }
413
414 pub fn substitute_templates(
418 &self,
419 bindings: &std::collections::HashMap<Arc<str>, Union>,
420 ) -> Union {
421 if bindings.is_empty() {
422 return self.clone();
423 }
424 let mut result = Union::empty();
425 result.possibly_undefined = self.possibly_undefined;
426 result.from_docblock = self.from_docblock;
427 for atomic in &self.types {
428 match atomic {
429 Atomic::TTemplateParam { name, .. } => {
430 if let Some(resolved) = bindings.get(name) {
431 for t in &resolved.types {
432 result.add_type(t.clone());
433 }
434 } else {
435 result.add_type(atomic.clone());
436 }
437 }
438 Atomic::TArray { key, value } => {
439 result.add_type(Atomic::TArray {
440 key: Box::new(key.substitute_templates(bindings)),
441 value: Box::new(value.substitute_templates(bindings)),
442 });
443 }
444 Atomic::TList { value } => {
445 result.add_type(Atomic::TList {
446 value: Box::new(value.substitute_templates(bindings)),
447 });
448 }
449 Atomic::TNonEmptyArray { key, value } => {
450 result.add_type(Atomic::TNonEmptyArray {
451 key: Box::new(key.substitute_templates(bindings)),
452 value: Box::new(value.substitute_templates(bindings)),
453 });
454 }
455 Atomic::TNonEmptyList { value } => {
456 result.add_type(Atomic::TNonEmptyList {
457 value: Box::new(value.substitute_templates(bindings)),
458 });
459 }
460 Atomic::TKeyedArray {
461 properties,
462 is_open,
463 is_list,
464 } => {
465 use crate::atomic::KeyedProperty;
466 let new_props = properties
467 .iter()
468 .map(|(k, prop)| {
469 (
470 k.clone(),
471 KeyedProperty {
472 ty: prop.ty.substitute_templates(bindings),
473 optional: prop.optional,
474 },
475 )
476 })
477 .collect();
478 result.add_type(Atomic::TKeyedArray {
479 properties: new_props,
480 is_open: *is_open,
481 is_list: *is_list,
482 });
483 }
484 Atomic::TCallable {
485 params,
486 return_type,
487 } => {
488 result.add_type(Atomic::TCallable {
489 params: params.as_ref().map(|ps| {
490 ps.iter()
491 .map(|p| substitute_in_fn_param(p, bindings))
492 .collect()
493 }),
494 return_type: return_type
495 .as_ref()
496 .map(|r| Box::new(r.substitute_templates(bindings))),
497 });
498 }
499 Atomic::TClosure {
500 params,
501 return_type,
502 this_type,
503 } => {
504 result.add_type(Atomic::TClosure {
505 params: params
506 .iter()
507 .map(|p| substitute_in_fn_param(p, bindings))
508 .collect(),
509 return_type: Box::new(return_type.substitute_templates(bindings)),
510 this_type: this_type
511 .as_ref()
512 .map(|t| Box::new(t.substitute_templates(bindings))),
513 });
514 }
515 Atomic::TConditional {
516 subject,
517 if_true,
518 if_false,
519 } => {
520 result.add_type(Atomic::TConditional {
521 subject: Box::new(subject.substitute_templates(bindings)),
522 if_true: Box::new(if_true.substitute_templates(bindings)),
523 if_false: Box::new(if_false.substitute_templates(bindings)),
524 });
525 }
526 Atomic::TIntersection { parts } => {
527 result.add_type(Atomic::TIntersection {
528 parts: parts
529 .iter()
530 .map(|p| p.substitute_templates(bindings))
531 .collect(),
532 });
533 }
534 Atomic::TNamedObject { fqcn, type_params } => {
535 if type_params.is_empty() && !fqcn.contains('\\') {
542 if let Some(resolved) = bindings.get(fqcn.as_ref()) {
543 for t in &resolved.types {
544 result.add_type(t.clone());
545 }
546 continue;
547 }
548 }
549 let new_params = type_params
550 .iter()
551 .map(|p| p.substitute_templates(bindings))
552 .collect();
553 result.add_type(Atomic::TNamedObject {
554 fqcn: fqcn.clone(),
555 type_params: new_params,
556 });
557 }
558 _ => {
559 result.add_type(atomic.clone());
560 }
561 }
562 }
563 result
564 }
565
566 pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
572 if other.is_mixed() {
573 return true;
574 }
575 if self.is_never() {
576 return true; }
578 self.types
579 .iter()
580 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
581 }
582
583 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
586 let mut result = Union::empty();
587 result.possibly_undefined = self.possibly_undefined;
588 result.from_docblock = self.from_docblock;
589 for atomic in &self.types {
590 if f(atomic) {
591 result.types.push(atomic.clone());
592 }
593 }
594 result
595 }
596
597 pub fn possibly_undefined(mut self) -> Self {
599 self.possibly_undefined = true;
600 self
601 }
602
603 pub fn from_docblock(mut self) -> Self {
605 self.from_docblock = true;
606 self
607 }
608}
609
610fn substitute_in_fn_param(
615 p: &crate::atomic::FnParam,
616 bindings: &std::collections::HashMap<Arc<str>, Union>,
617) -> crate::atomic::FnParam {
618 crate::atomic::FnParam {
619 name: p.name.clone(),
620 ty: p.ty.as_ref().map(|t| {
621 let u = t.to_union();
622 let substituted = u.substitute_templates(bindings);
623 crate::compact::SimpleType::from_union(substituted)
624 }),
625 default: p.default.as_ref().map(|d| {
626 let u = d.to_union();
627 let substituted = u.substitute_templates(bindings);
628 crate::compact::SimpleType::from_union(substituted)
629 }),
630 is_variadic: p.is_variadic,
631 is_byref: p.is_byref,
632 is_optional: p.is_optional,
633 }
634}
635
636fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
641 if sub == sup {
642 return true;
643 }
644 match (sub, sup) {
645 (Atomic::TNever, _) => true,
647 (_, Atomic::TMixed) => true,
649 (Atomic::TMixed, _) => true,
650
651 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
653 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
654 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
655 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
656 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
657 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
658 (Atomic::TPositiveInt, Atomic::TInt) => true,
659 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
660 (Atomic::TNegativeInt, Atomic::TInt) => true,
661 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
662 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
663
664 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
665 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
666 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
667
668 (Atomic::TLiteralString(s), Atomic::TString) => {
669 let _ = s;
670 true
671 }
672 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
673 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
674 (Atomic::TNonEmptyString, Atomic::TString) => true,
675 (Atomic::TNumericString, Atomic::TString) => true,
676 (Atomic::TClassString(_), Atomic::TString) => true,
677 (Atomic::TInterfaceString, Atomic::TString) => true,
678 (Atomic::TEnumString, Atomic::TString) => true,
679 (Atomic::TTraitString, Atomic::TString) => true,
680
681 (Atomic::TTrue, Atomic::TBool) => true,
682 (Atomic::TFalse, Atomic::TBool) => true,
683
684 (Atomic::TInt, Atomic::TNumeric) => true,
685 (Atomic::TFloat, Atomic::TNumeric) => true,
686 (Atomic::TNumericString, Atomic::TNumeric) => true,
687
688 (Atomic::TInt, Atomic::TScalar) => true,
689 (Atomic::TFloat, Atomic::TScalar) => true,
690 (Atomic::TString, Atomic::TScalar) => true,
691 (Atomic::TBool, Atomic::TScalar) => true,
692 (Atomic::TNumeric, Atomic::TScalar) => true,
693 (Atomic::TTrue, Atomic::TScalar) => true,
694 (Atomic::TFalse, Atomic::TScalar) => true,
695
696 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
698 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
699 (Atomic::TSelf { .. }, Atomic::TObject) => true,
700 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
702 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
703 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
705 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
706
707 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
709 (Atomic::TPositiveInt, Atomic::TFloat) => true,
710 (Atomic::TInt, Atomic::TFloat) => true,
711
712 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
714
715 (Atomic::TString, Atomic::TCallable { .. }) => true,
717 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
718 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
719 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
720 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
721
722 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
724 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
726 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
728 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
730 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
732 fqcn.as_ref().eq_ignore_ascii_case("closure")
733 }
734 (Atomic::TClosure { .. }, Atomic::TObject) => true,
735
736 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
738 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
740 && value.is_subtype_of_simple(av)
741 }
742 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
743 value.is_subtype_of_simple(lv)
744 }
745 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
747 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
748 && av.is_subtype_of_simple(lv)
749 }
750 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
751 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
752 && av.is_subtype_of_simple(lv)
753 }
754 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
755 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
756 && av.is_subtype_of_simple(lv)
757 }
758 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
759 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
760 && av.is_subtype_of_simple(lv)
761 }
762 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
764 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
765 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
766 }
767
768 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
770 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
771 }
772
773 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
775
776 (
778 Atomic::TKeyedArray {
779 properties,
780 is_list,
781 ..
782 },
783 Atomic::TList { value: lv },
784 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
785 (
786 Atomic::TKeyedArray {
787 properties,
788 is_list,
789 ..
790 },
791 Atomic::TNonEmptyList { value: lv },
792 ) => {
793 *is_list
794 && !properties.is_empty()
795 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
796 }
797
798 (_, Atomic::TTemplateParam { .. }) => true,
800
801 _ => false,
802 }
803}
804
805#[cfg(test)]
810mod tests {
811 use super::*;
812
813 #[test]
814 fn single_is_single() {
815 let u = Union::single(Atomic::TString);
816 assert!(u.is_single());
817 assert!(!u.is_nullable());
818 }
819
820 #[test]
821 fn nullable_has_null() {
822 let u = Union::nullable(Atomic::TString);
823 assert!(u.is_nullable());
824 assert_eq!(u.types.len(), 2);
825 }
826
827 #[test]
828 fn add_type_deduplicates() {
829 let mut u = Union::single(Atomic::TString);
830 u.add_type(Atomic::TString);
831 assert_eq!(u.types.len(), 1);
832 }
833
834 #[test]
835 fn add_type_literal_subsumed_by_base() {
836 let mut u = Union::single(Atomic::TInt);
837 u.add_type(Atomic::TLiteralInt(42));
838 assert_eq!(u.types.len(), 1);
839 assert!(matches!(u.types[0], Atomic::TInt));
840 }
841
842 #[test]
843 fn add_type_base_widens_literals() {
844 let mut u = Union::single(Atomic::TLiteralInt(1));
845 u.add_type(Atomic::TLiteralInt(2));
846 u.add_type(Atomic::TInt);
847 assert_eq!(u.types.len(), 1);
848 assert!(matches!(u.types[0], Atomic::TInt));
849 }
850
851 #[test]
852 fn mixed_subsumes_everything() {
853 let mut u = Union::single(Atomic::TString);
854 u.add_type(Atomic::TMixed);
855 assert_eq!(u.types.len(), 1);
856 assert!(u.is_mixed());
857 }
858
859 #[test]
860 fn remove_null() {
861 let u = Union::nullable(Atomic::TString);
862 let narrowed = u.remove_null();
863 assert!(!narrowed.is_nullable());
864 assert_eq!(narrowed.types.len(), 1);
865 }
866
867 #[test]
868 fn narrow_to_truthy_removes_null_false() {
869 let mut u = Union::empty();
870 u.add_type(Atomic::TString);
871 u.add_type(Atomic::TNull);
872 u.add_type(Atomic::TFalse);
873 let truthy = u.narrow_to_truthy();
874 assert!(!truthy.is_nullable());
875 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
876 }
877
878 #[test]
879 fn merge_combines_types() {
880 let a = Union::single(Atomic::TString);
881 let b = Union::single(Atomic::TInt);
882 let merged = Union::merge(&a, &b);
883 assert_eq!(merged.types.len(), 2);
884 }
885
886 #[test]
887 fn subtype_literal_int_under_int() {
888 let sub = Union::single(Atomic::TLiteralInt(5));
889 let sup = Union::single(Atomic::TInt);
890 assert!(sub.is_subtype_of_simple(&sup));
891 }
892
893 #[test]
894 fn subtype_never_is_bottom() {
895 let never = Union::never();
896 let string = Union::single(Atomic::TString);
897 assert!(never.is_subtype_of_simple(&string));
898 }
899
900 #[test]
901 fn subtype_everything_under_mixed() {
902 let string = Union::single(Atomic::TString);
903 let mixed = Union::mixed();
904 assert!(string.is_subtype_of_simple(&mixed));
905 }
906
907 #[test]
908 fn template_substitution() {
909 let mut bindings = std::collections::HashMap::new();
910 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
911
912 let tmpl = Union::single(Atomic::TTemplateParam {
913 name: Arc::from("T"),
914 as_type: Box::new(Union::mixed()),
915 defining_entity: Arc::from("MyClass"),
916 });
917
918 let resolved = tmpl.substitute_templates(&bindings);
919 assert_eq!(resolved.types.len(), 1);
920 assert!(matches!(resolved.types[0], Atomic::TString));
921 }
922
923 #[test]
924 fn intersection_is_object() {
925 let parts = vec![
926 Union::single(Atomic::TNamedObject {
927 fqcn: Arc::from("Iterator"),
928 type_params: vec![],
929 }),
930 Union::single(Atomic::TNamedObject {
931 fqcn: Arc::from("Countable"),
932 type_params: vec![],
933 }),
934 ];
935 let atomic = Atomic::TIntersection { parts };
936 assert!(atomic.is_object());
937 assert!(!atomic.can_be_falsy());
938 assert!(atomic.can_be_truthy());
939 }
940
941 #[test]
942 fn intersection_display_two_parts() {
943 let parts = vec![
944 Union::single(Atomic::TNamedObject {
945 fqcn: Arc::from("Iterator"),
946 type_params: vec![],
947 }),
948 Union::single(Atomic::TNamedObject {
949 fqcn: Arc::from("Countable"),
950 type_params: vec![],
951 }),
952 ];
953 let u = Union::single(Atomic::TIntersection { parts });
954 assert_eq!(format!("{u}"), "Iterator&Countable");
955 }
956
957 #[test]
958 fn intersection_display_three_parts() {
959 let parts = vec![
960 Union::single(Atomic::TNamedObject {
961 fqcn: Arc::from("A"),
962 type_params: vec![],
963 }),
964 Union::single(Atomic::TNamedObject {
965 fqcn: Arc::from("B"),
966 type_params: vec![],
967 }),
968 Union::single(Atomic::TNamedObject {
969 fqcn: Arc::from("C"),
970 type_params: vec![],
971 }),
972 ];
973 let u = Union::single(Atomic::TIntersection { parts });
974 assert_eq!(format!("{u}"), "A&B&C");
975 }
976
977 #[test]
978 fn intersection_in_nullable_union_display() {
979 let intersection = Atomic::TIntersection {
980 parts: vec![
981 Union::single(Atomic::TNamedObject {
982 fqcn: Arc::from("Iterator"),
983 type_params: vec![],
984 }),
985 Union::single(Atomic::TNamedObject {
986 fqcn: Arc::from("Countable"),
987 type_params: vec![],
988 }),
989 ],
990 };
991 let mut u = Union::single(intersection);
992 u.add_type(Atomic::TNull);
993 assert!(u.is_nullable());
994 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
995 }
996
997 fn t_param(name: &str) -> Union {
1000 Union::single(Atomic::TTemplateParam {
1001 name: Arc::from(name),
1002 as_type: Box::new(Union::mixed()),
1003 defining_entity: Arc::from("Fn"),
1004 })
1005 }
1006
1007 fn bindings_t_string() -> std::collections::HashMap<Arc<str>, Union> {
1008 let mut b = std::collections::HashMap::new();
1009 b.insert(Arc::from("T"), Union::single(Atomic::TString));
1010 b
1011 }
1012
1013 #[test]
1014 fn substitute_non_empty_array_key_and_value() {
1015 let ty = Union::single(Atomic::TNonEmptyArray {
1016 key: Box::new(t_param("T")),
1017 value: Box::new(t_param("T")),
1018 });
1019 let result = ty.substitute_templates(&bindings_t_string());
1020 assert_eq!(result.types.len(), 1);
1021 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1022 panic!("expected TNonEmptyArray");
1023 };
1024 assert!(matches!(key.types[0], Atomic::TString));
1025 assert!(matches!(value.types[0], Atomic::TString));
1026 }
1027
1028 #[test]
1029 fn substitute_non_empty_list_value() {
1030 let ty = Union::single(Atomic::TNonEmptyList {
1031 value: Box::new(t_param("T")),
1032 });
1033 let result = ty.substitute_templates(&bindings_t_string());
1034 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1035 panic!("expected TNonEmptyList");
1036 };
1037 assert!(matches!(value.types[0], Atomic::TString));
1038 }
1039
1040 #[test]
1041 fn substitute_keyed_array_property_types() {
1042 use crate::atomic::{ArrayKey, KeyedProperty};
1043 use indexmap::IndexMap;
1044 let mut props = IndexMap::new();
1045 props.insert(
1046 ArrayKey::String(Arc::from("name")),
1047 KeyedProperty {
1048 ty: t_param("T"),
1049 optional: false,
1050 },
1051 );
1052 props.insert(
1053 ArrayKey::String(Arc::from("tag")),
1054 KeyedProperty {
1055 ty: t_param("T"),
1056 optional: true,
1057 },
1058 );
1059 let ty = Union::single(Atomic::TKeyedArray {
1060 properties: props,
1061 is_open: true,
1062 is_list: false,
1063 });
1064 let result = ty.substitute_templates(&bindings_t_string());
1065 let Atomic::TKeyedArray {
1066 properties,
1067 is_open,
1068 is_list,
1069 } = &result.types[0]
1070 else {
1071 panic!("expected TKeyedArray");
1072 };
1073 assert!(is_open);
1074 assert!(!is_list);
1075 assert!(matches!(
1076 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1077 Atomic::TString
1078 ));
1079 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1080 assert!(matches!(
1081 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1082 Atomic::TString
1083 ));
1084 }
1085
1086 #[test]
1087 fn substitute_callable_params_and_return() {
1088 use crate::atomic::FnParam;
1089 let ty = Union::single(Atomic::TCallable {
1090 params: Some(vec![FnParam {
1091 name: Arc::from("x"),
1092 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1093 default: None,
1094 is_variadic: false,
1095 is_byref: false,
1096 is_optional: false,
1097 }]),
1098 return_type: Some(Box::new(t_param("T"))),
1099 });
1100 let result = ty.substitute_templates(&bindings_t_string());
1101 let Atomic::TCallable {
1102 params,
1103 return_type,
1104 } = &result.types[0]
1105 else {
1106 panic!("expected TCallable");
1107 };
1108 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1109 let param_union = param_ty.to_union();
1110 assert!(matches!(param_union.types[0], Atomic::TString));
1111 let ret = return_type.as_ref().unwrap();
1112 assert!(matches!(ret.types[0], Atomic::TString));
1113 }
1114
1115 #[test]
1116 fn substitute_callable_bare_no_panic() {
1117 let ty = Union::single(Atomic::TCallable {
1119 params: None,
1120 return_type: None,
1121 });
1122 let result = ty.substitute_templates(&bindings_t_string());
1123 assert!(matches!(
1124 result.types[0],
1125 Atomic::TCallable {
1126 params: None,
1127 return_type: None
1128 }
1129 ));
1130 }
1131
1132 #[test]
1133 fn substitute_closure_params_return_and_this() {
1134 use crate::atomic::FnParam;
1135 let ty = Union::single(Atomic::TClosure {
1136 params: vec![FnParam {
1137 name: Arc::from("a"),
1138 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1139 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1140 is_variadic: true,
1141 is_byref: true,
1142 is_optional: true,
1143 }],
1144 return_type: Box::new(t_param("T")),
1145 this_type: Some(Box::new(t_param("T"))),
1146 });
1147 let result = ty.substitute_templates(&bindings_t_string());
1148 let Atomic::TClosure {
1149 params,
1150 return_type,
1151 this_type,
1152 } = &result.types[0]
1153 else {
1154 panic!("expected TClosure");
1155 };
1156 let p = ¶ms[0];
1157 let ty_union = p.ty.as_ref().unwrap().to_union();
1158 let default_union = p.default.as_ref().unwrap().to_union();
1159 assert!(matches!(ty_union.types[0], Atomic::TString));
1160 assert!(matches!(default_union.types[0], Atomic::TString));
1161 assert!(p.is_variadic);
1163 assert!(p.is_byref);
1164 assert!(p.is_optional);
1165 assert!(matches!(return_type.types[0], Atomic::TString));
1166 assert!(matches!(
1167 this_type.as_ref().unwrap().types[0],
1168 Atomic::TString
1169 ));
1170 }
1171
1172 #[test]
1173 fn substitute_conditional_all_branches() {
1174 let ty = Union::single(Atomic::TConditional {
1175 subject: Box::new(t_param("T")),
1176 if_true: Box::new(t_param("T")),
1177 if_false: Box::new(Union::single(Atomic::TInt)),
1178 });
1179 let result = ty.substitute_templates(&bindings_t_string());
1180 let Atomic::TConditional {
1181 subject,
1182 if_true,
1183 if_false,
1184 } = &result.types[0]
1185 else {
1186 panic!("expected TConditional");
1187 };
1188 assert!(matches!(subject.types[0], Atomic::TString));
1189 assert!(matches!(if_true.types[0], Atomic::TString));
1190 assert!(matches!(if_false.types[0], Atomic::TInt));
1191 }
1192
1193 #[test]
1194 fn substitute_intersection_parts() {
1195 let ty = Union::single(Atomic::TIntersection {
1196 parts: vec![
1197 Union::single(Atomic::TNamedObject {
1198 fqcn: Arc::from("Countable"),
1199 type_params: vec![],
1200 }),
1201 t_param("T"),
1202 ],
1203 });
1204 let result = ty.substitute_templates(&bindings_t_string());
1205 let Atomic::TIntersection { parts } = &result.types[0] else {
1206 panic!("expected TIntersection");
1207 };
1208 assert_eq!(parts.len(), 2);
1209 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1210 assert!(matches!(parts[1].types[0], Atomic::TString));
1211 }
1212
1213 #[test]
1214 fn substitute_no_template_params_identity() {
1215 let ty = Union::single(Atomic::TInt);
1216 let result = ty.substitute_templates(&bindings_t_string());
1217 assert!(matches!(result.types[0], Atomic::TInt));
1218 }
1219}