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 merge(a: &Union, b: &Union) -> Union {
337 let mut result = a.clone();
338 for atomic in &b.types {
339 result.add_type(atomic.clone());
340 }
341 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
342 result
343 }
344
345 pub fn intersect_with(&self, other: &Union) -> Union {
349 if self.is_mixed() {
350 return other.clone();
351 }
352 if other.is_mixed() {
353 return self.clone();
354 }
355 let mut result = Union::empty();
357 for a in &self.types {
358 for b in &other.types {
359 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
360 result.add_type(a.clone());
361 break;
362 }
363 }
364 }
365 if result.is_empty() {
367 other.clone()
368 } else {
369 result
370 }
371 }
372
373 pub fn substitute_templates(
377 &self,
378 bindings: &std::collections::HashMap<Arc<str>, Union>,
379 ) -> Union {
380 if bindings.is_empty() {
381 return self.clone();
382 }
383 let mut result = Union::empty();
384 result.possibly_undefined = self.possibly_undefined;
385 result.from_docblock = self.from_docblock;
386 for atomic in &self.types {
387 match atomic {
388 Atomic::TTemplateParam { name, .. } => {
389 if let Some(resolved) = bindings.get(name) {
390 for t in &resolved.types {
391 result.add_type(t.clone());
392 }
393 } else {
394 result.add_type(atomic.clone());
395 }
396 }
397 Atomic::TArray { key, value } => {
398 result.add_type(Atomic::TArray {
399 key: Box::new(key.substitute_templates(bindings)),
400 value: Box::new(value.substitute_templates(bindings)),
401 });
402 }
403 Atomic::TList { value } => {
404 result.add_type(Atomic::TList {
405 value: Box::new(value.substitute_templates(bindings)),
406 });
407 }
408 Atomic::TNonEmptyArray { key, value } => {
409 result.add_type(Atomic::TNonEmptyArray {
410 key: Box::new(key.substitute_templates(bindings)),
411 value: Box::new(value.substitute_templates(bindings)),
412 });
413 }
414 Atomic::TNonEmptyList { value } => {
415 result.add_type(Atomic::TNonEmptyList {
416 value: Box::new(value.substitute_templates(bindings)),
417 });
418 }
419 Atomic::TKeyedArray {
420 properties,
421 is_open,
422 is_list,
423 } => {
424 use crate::atomic::KeyedProperty;
425 let new_props = properties
426 .iter()
427 .map(|(k, prop)| {
428 (
429 k.clone(),
430 KeyedProperty {
431 ty: prop.ty.substitute_templates(bindings),
432 optional: prop.optional,
433 },
434 )
435 })
436 .collect();
437 result.add_type(Atomic::TKeyedArray {
438 properties: new_props,
439 is_open: *is_open,
440 is_list: *is_list,
441 });
442 }
443 Atomic::TCallable {
444 params,
445 return_type,
446 } => {
447 result.add_type(Atomic::TCallable {
448 params: params.as_ref().map(|ps| {
449 ps.iter()
450 .map(|p| substitute_in_fn_param(p, bindings))
451 .collect()
452 }),
453 return_type: return_type
454 .as_ref()
455 .map(|r| Box::new(r.substitute_templates(bindings))),
456 });
457 }
458 Atomic::TClosure {
459 params,
460 return_type,
461 this_type,
462 } => {
463 result.add_type(Atomic::TClosure {
464 params: params
465 .iter()
466 .map(|p| substitute_in_fn_param(p, bindings))
467 .collect(),
468 return_type: Box::new(return_type.substitute_templates(bindings)),
469 this_type: this_type
470 .as_ref()
471 .map(|t| Box::new(t.substitute_templates(bindings))),
472 });
473 }
474 Atomic::TConditional {
475 subject,
476 if_true,
477 if_false,
478 } => {
479 result.add_type(Atomic::TConditional {
480 subject: Box::new(subject.substitute_templates(bindings)),
481 if_true: Box::new(if_true.substitute_templates(bindings)),
482 if_false: Box::new(if_false.substitute_templates(bindings)),
483 });
484 }
485 Atomic::TIntersection { parts } => {
486 result.add_type(Atomic::TIntersection {
487 parts: parts
488 .iter()
489 .map(|p| p.substitute_templates(bindings))
490 .collect(),
491 });
492 }
493 Atomic::TNamedObject { fqcn, type_params } => {
494 if type_params.is_empty() && !fqcn.contains('\\') {
501 if let Some(resolved) = bindings.get(fqcn.as_ref()) {
502 for t in &resolved.types {
503 result.add_type(t.clone());
504 }
505 continue;
506 }
507 }
508 let new_params = type_params
509 .iter()
510 .map(|p| p.substitute_templates(bindings))
511 .collect();
512 result.add_type(Atomic::TNamedObject {
513 fqcn: fqcn.clone(),
514 type_params: new_params,
515 });
516 }
517 _ => {
518 result.add_type(atomic.clone());
519 }
520 }
521 }
522 result
523 }
524
525 pub fn is_subtype_of_simple(&self, other: &Union) -> bool {
531 if other.is_mixed() {
532 return true;
533 }
534 if self.is_never() {
535 return true; }
537 self.types
538 .iter()
539 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
540 }
541
542 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Union {
545 let mut result = Union::empty();
546 result.possibly_undefined = self.possibly_undefined;
547 result.from_docblock = self.from_docblock;
548 for atomic in &self.types {
549 if f(atomic) {
550 result.types.push(atomic.clone());
551 }
552 }
553 result
554 }
555
556 pub fn possibly_undefined(mut self) -> Self {
558 self.possibly_undefined = true;
559 self
560 }
561
562 pub fn from_docblock(mut self) -> Self {
564 self.from_docblock = true;
565 self
566 }
567}
568
569fn substitute_in_fn_param(
574 p: &crate::atomic::FnParam,
575 bindings: &std::collections::HashMap<Arc<str>, Union>,
576) -> crate::atomic::FnParam {
577 crate::atomic::FnParam {
578 name: p.name.clone(),
579 ty: p.ty.as_ref().map(|t| {
580 let u = t.to_union();
581 let substituted = u.substitute_templates(bindings);
582 crate::compact::SimpleType::from_union(substituted)
583 }),
584 default: p.default.as_ref().map(|d| {
585 let u = d.to_union();
586 let substituted = u.substitute_templates(bindings);
587 crate::compact::SimpleType::from_union(substituted)
588 }),
589 is_variadic: p.is_variadic,
590 is_byref: p.is_byref,
591 is_optional: p.is_optional,
592 }
593}
594
595fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
600 if sub == sup {
601 return true;
602 }
603 match (sub, sup) {
604 (Atomic::TNever, _) => true,
606 (_, Atomic::TMixed) => true,
608 (Atomic::TMixed, _) => true,
609
610 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
612 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
613 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
614 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
615 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
616 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
617 (Atomic::TPositiveInt, Atomic::TInt) => true,
618 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
619 (Atomic::TNegativeInt, Atomic::TInt) => true,
620 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
621 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
622
623 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
624 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
625 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
626
627 (Atomic::TLiteralString(s), Atomic::TString) => {
628 let _ = s;
629 true
630 }
631 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
632 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
633 (Atomic::TNonEmptyString, Atomic::TString) => true,
634 (Atomic::TNumericString, Atomic::TString) => true,
635 (Atomic::TClassString(_), Atomic::TString) => true,
636 (Atomic::TInterfaceString, Atomic::TString) => true,
637 (Atomic::TEnumString, Atomic::TString) => true,
638 (Atomic::TTraitString, Atomic::TString) => true,
639
640 (Atomic::TTrue, Atomic::TBool) => true,
641 (Atomic::TFalse, Atomic::TBool) => true,
642
643 (Atomic::TInt, Atomic::TNumeric) => true,
644 (Atomic::TFloat, Atomic::TNumeric) => true,
645 (Atomic::TNumericString, Atomic::TNumeric) => true,
646
647 (Atomic::TInt, Atomic::TScalar) => true,
648 (Atomic::TFloat, Atomic::TScalar) => true,
649 (Atomic::TString, Atomic::TScalar) => true,
650 (Atomic::TBool, Atomic::TScalar) => true,
651 (Atomic::TNumeric, Atomic::TScalar) => true,
652 (Atomic::TTrue, Atomic::TScalar) => true,
653 (Atomic::TFalse, Atomic::TScalar) => true,
654
655 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
657 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
658 (Atomic::TSelf { .. }, Atomic::TObject) => true,
659 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
661 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
662 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
664 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
665
666 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
668 (Atomic::TPositiveInt, Atomic::TFloat) => true,
669 (Atomic::TInt, Atomic::TFloat) => true,
670
671 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
673
674 (Atomic::TString, Atomic::TCallable { .. }) => true,
676 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
677 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
678 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
679 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
680
681 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
683 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
685 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
687 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
689 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
691 fqcn.as_ref().eq_ignore_ascii_case("closure")
692 }
693 (Atomic::TClosure { .. }, Atomic::TObject) => true,
694
695 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
697 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
699 && value.is_subtype_of_simple(av)
700 }
701 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
702 value.is_subtype_of_simple(lv)
703 }
704 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
706 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
707 && av.is_subtype_of_simple(lv)
708 }
709 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
710 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
711 && av.is_subtype_of_simple(lv)
712 }
713 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
714 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
715 && av.is_subtype_of_simple(lv)
716 }
717 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
718 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
719 && av.is_subtype_of_simple(lv)
720 }
721 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_of_simple(v2),
723 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
724 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
725 }
726
727 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
729 k1.is_subtype_of_simple(k2) && v1.is_subtype_of_simple(v2)
730 }
731
732 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
734
735 (
737 Atomic::TKeyedArray {
738 properties,
739 is_list,
740 ..
741 },
742 Atomic::TList { value: lv },
743 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_of_simple(lv)),
744 (
745 Atomic::TKeyedArray {
746 properties,
747 is_list,
748 ..
749 },
750 Atomic::TNonEmptyList { value: lv },
751 ) => {
752 *is_list
753 && !properties.is_empty()
754 && properties.values().all(|p| p.ty.is_subtype_of_simple(lv))
755 }
756
757 (_, Atomic::TTemplateParam { .. }) => true,
759
760 _ => false,
761 }
762}
763
764#[cfg(test)]
769mod tests {
770 use super::*;
771
772 #[test]
773 fn single_is_single() {
774 let u = Union::single(Atomic::TString);
775 assert!(u.is_single());
776 assert!(!u.is_nullable());
777 }
778
779 #[test]
780 fn nullable_has_null() {
781 let u = Union::nullable(Atomic::TString);
782 assert!(u.is_nullable());
783 assert_eq!(u.types.len(), 2);
784 }
785
786 #[test]
787 fn add_type_deduplicates() {
788 let mut u = Union::single(Atomic::TString);
789 u.add_type(Atomic::TString);
790 assert_eq!(u.types.len(), 1);
791 }
792
793 #[test]
794 fn add_type_literal_subsumed_by_base() {
795 let mut u = Union::single(Atomic::TInt);
796 u.add_type(Atomic::TLiteralInt(42));
797 assert_eq!(u.types.len(), 1);
798 assert!(matches!(u.types[0], Atomic::TInt));
799 }
800
801 #[test]
802 fn add_type_base_widens_literals() {
803 let mut u = Union::single(Atomic::TLiteralInt(1));
804 u.add_type(Atomic::TLiteralInt(2));
805 u.add_type(Atomic::TInt);
806 assert_eq!(u.types.len(), 1);
807 assert!(matches!(u.types[0], Atomic::TInt));
808 }
809
810 #[test]
811 fn mixed_subsumes_everything() {
812 let mut u = Union::single(Atomic::TString);
813 u.add_type(Atomic::TMixed);
814 assert_eq!(u.types.len(), 1);
815 assert!(u.is_mixed());
816 }
817
818 #[test]
819 fn remove_null() {
820 let u = Union::nullable(Atomic::TString);
821 let narrowed = u.remove_null();
822 assert!(!narrowed.is_nullable());
823 assert_eq!(narrowed.types.len(), 1);
824 }
825
826 #[test]
827 fn narrow_to_truthy_removes_null_false() {
828 let mut u = Union::empty();
829 u.add_type(Atomic::TString);
830 u.add_type(Atomic::TNull);
831 u.add_type(Atomic::TFalse);
832 let truthy = u.narrow_to_truthy();
833 assert!(!truthy.is_nullable());
834 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
835 }
836
837 #[test]
838 fn merge_combines_types() {
839 let a = Union::single(Atomic::TString);
840 let b = Union::single(Atomic::TInt);
841 let merged = Union::merge(&a, &b);
842 assert_eq!(merged.types.len(), 2);
843 }
844
845 #[test]
846 fn subtype_literal_int_under_int() {
847 let sub = Union::single(Atomic::TLiteralInt(5));
848 let sup = Union::single(Atomic::TInt);
849 assert!(sub.is_subtype_of_simple(&sup));
850 }
851
852 #[test]
853 fn subtype_never_is_bottom() {
854 let never = Union::never();
855 let string = Union::single(Atomic::TString);
856 assert!(never.is_subtype_of_simple(&string));
857 }
858
859 #[test]
860 fn subtype_everything_under_mixed() {
861 let string = Union::single(Atomic::TString);
862 let mixed = Union::mixed();
863 assert!(string.is_subtype_of_simple(&mixed));
864 }
865
866 #[test]
867 fn template_substitution() {
868 let mut bindings = std::collections::HashMap::new();
869 bindings.insert(Arc::from("T"), Union::single(Atomic::TString));
870
871 let tmpl = Union::single(Atomic::TTemplateParam {
872 name: Arc::from("T"),
873 as_type: Box::new(Union::mixed()),
874 defining_entity: Arc::from("MyClass"),
875 });
876
877 let resolved = tmpl.substitute_templates(&bindings);
878 assert_eq!(resolved.types.len(), 1);
879 assert!(matches!(resolved.types[0], Atomic::TString));
880 }
881
882 #[test]
883 fn intersection_is_object() {
884 let parts = vec![
885 Union::single(Atomic::TNamedObject {
886 fqcn: Arc::from("Iterator"),
887 type_params: vec![],
888 }),
889 Union::single(Atomic::TNamedObject {
890 fqcn: Arc::from("Countable"),
891 type_params: vec![],
892 }),
893 ];
894 let atomic = Atomic::TIntersection { parts };
895 assert!(atomic.is_object());
896 assert!(!atomic.can_be_falsy());
897 assert!(atomic.can_be_truthy());
898 }
899
900 #[test]
901 fn intersection_display_two_parts() {
902 let parts = vec![
903 Union::single(Atomic::TNamedObject {
904 fqcn: Arc::from("Iterator"),
905 type_params: vec![],
906 }),
907 Union::single(Atomic::TNamedObject {
908 fqcn: Arc::from("Countable"),
909 type_params: vec![],
910 }),
911 ];
912 let u = Union::single(Atomic::TIntersection { parts });
913 assert_eq!(format!("{u}"), "Iterator&Countable");
914 }
915
916 #[test]
917 fn intersection_display_three_parts() {
918 let parts = vec![
919 Union::single(Atomic::TNamedObject {
920 fqcn: Arc::from("A"),
921 type_params: vec![],
922 }),
923 Union::single(Atomic::TNamedObject {
924 fqcn: Arc::from("B"),
925 type_params: vec![],
926 }),
927 Union::single(Atomic::TNamedObject {
928 fqcn: Arc::from("C"),
929 type_params: vec![],
930 }),
931 ];
932 let u = Union::single(Atomic::TIntersection { parts });
933 assert_eq!(format!("{u}"), "A&B&C");
934 }
935
936 #[test]
937 fn intersection_in_nullable_union_display() {
938 let intersection = Atomic::TIntersection {
939 parts: vec![
940 Union::single(Atomic::TNamedObject {
941 fqcn: Arc::from("Iterator"),
942 type_params: vec![],
943 }),
944 Union::single(Atomic::TNamedObject {
945 fqcn: Arc::from("Countable"),
946 type_params: vec![],
947 }),
948 ],
949 };
950 let mut u = Union::single(intersection);
951 u.add_type(Atomic::TNull);
952 assert!(u.is_nullable());
953 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
954 }
955
956 fn t_param(name: &str) -> Union {
959 Union::single(Atomic::TTemplateParam {
960 name: Arc::from(name),
961 as_type: Box::new(Union::mixed()),
962 defining_entity: Arc::from("Fn"),
963 })
964 }
965
966 fn bindings_t_string() -> std::collections::HashMap<Arc<str>, Union> {
967 let mut b = std::collections::HashMap::new();
968 b.insert(Arc::from("T"), Union::single(Atomic::TString));
969 b
970 }
971
972 #[test]
973 fn substitute_non_empty_array_key_and_value() {
974 let ty = Union::single(Atomic::TNonEmptyArray {
975 key: Box::new(t_param("T")),
976 value: Box::new(t_param("T")),
977 });
978 let result = ty.substitute_templates(&bindings_t_string());
979 assert_eq!(result.types.len(), 1);
980 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
981 panic!("expected TNonEmptyArray");
982 };
983 assert!(matches!(key.types[0], Atomic::TString));
984 assert!(matches!(value.types[0], Atomic::TString));
985 }
986
987 #[test]
988 fn substitute_non_empty_list_value() {
989 let ty = Union::single(Atomic::TNonEmptyList {
990 value: Box::new(t_param("T")),
991 });
992 let result = ty.substitute_templates(&bindings_t_string());
993 let Atomic::TNonEmptyList { value } = &result.types[0] else {
994 panic!("expected TNonEmptyList");
995 };
996 assert!(matches!(value.types[0], Atomic::TString));
997 }
998
999 #[test]
1000 fn substitute_keyed_array_property_types() {
1001 use crate::atomic::{ArrayKey, KeyedProperty};
1002 use indexmap::IndexMap;
1003 let mut props = IndexMap::new();
1004 props.insert(
1005 ArrayKey::String(Arc::from("name")),
1006 KeyedProperty {
1007 ty: t_param("T"),
1008 optional: false,
1009 },
1010 );
1011 props.insert(
1012 ArrayKey::String(Arc::from("tag")),
1013 KeyedProperty {
1014 ty: t_param("T"),
1015 optional: true,
1016 },
1017 );
1018 let ty = Union::single(Atomic::TKeyedArray {
1019 properties: props,
1020 is_open: true,
1021 is_list: false,
1022 });
1023 let result = ty.substitute_templates(&bindings_t_string());
1024 let Atomic::TKeyedArray {
1025 properties,
1026 is_open,
1027 is_list,
1028 } = &result.types[0]
1029 else {
1030 panic!("expected TKeyedArray");
1031 };
1032 assert!(is_open);
1033 assert!(!is_list);
1034 assert!(matches!(
1035 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1036 Atomic::TString
1037 ));
1038 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1039 assert!(matches!(
1040 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1041 Atomic::TString
1042 ));
1043 }
1044
1045 #[test]
1046 fn substitute_callable_params_and_return() {
1047 use crate::atomic::FnParam;
1048 let ty = Union::single(Atomic::TCallable {
1049 params: Some(vec![FnParam {
1050 name: Arc::from("x"),
1051 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1052 default: None,
1053 is_variadic: false,
1054 is_byref: false,
1055 is_optional: false,
1056 }]),
1057 return_type: Some(Box::new(t_param("T"))),
1058 });
1059 let result = ty.substitute_templates(&bindings_t_string());
1060 let Atomic::TCallable {
1061 params,
1062 return_type,
1063 } = &result.types[0]
1064 else {
1065 panic!("expected TCallable");
1066 };
1067 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1068 let param_union = param_ty.to_union();
1069 assert!(matches!(param_union.types[0], Atomic::TString));
1070 let ret = return_type.as_ref().unwrap();
1071 assert!(matches!(ret.types[0], Atomic::TString));
1072 }
1073
1074 #[test]
1075 fn substitute_callable_bare_no_panic() {
1076 let ty = Union::single(Atomic::TCallable {
1078 params: None,
1079 return_type: None,
1080 });
1081 let result = ty.substitute_templates(&bindings_t_string());
1082 assert!(matches!(
1083 result.types[0],
1084 Atomic::TCallable {
1085 params: None,
1086 return_type: None
1087 }
1088 ));
1089 }
1090
1091 #[test]
1092 fn substitute_closure_params_return_and_this() {
1093 use crate::atomic::FnParam;
1094 let ty = Union::single(Atomic::TClosure {
1095 params: vec![FnParam {
1096 name: Arc::from("a"),
1097 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1098 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1099 is_variadic: true,
1100 is_byref: true,
1101 is_optional: true,
1102 }],
1103 return_type: Box::new(t_param("T")),
1104 this_type: Some(Box::new(t_param("T"))),
1105 });
1106 let result = ty.substitute_templates(&bindings_t_string());
1107 let Atomic::TClosure {
1108 params,
1109 return_type,
1110 this_type,
1111 } = &result.types[0]
1112 else {
1113 panic!("expected TClosure");
1114 };
1115 let p = ¶ms[0];
1116 let ty_union = p.ty.as_ref().unwrap().to_union();
1117 let default_union = p.default.as_ref().unwrap().to_union();
1118 assert!(matches!(ty_union.types[0], Atomic::TString));
1119 assert!(matches!(default_union.types[0], Atomic::TString));
1120 assert!(p.is_variadic);
1122 assert!(p.is_byref);
1123 assert!(p.is_optional);
1124 assert!(matches!(return_type.types[0], Atomic::TString));
1125 assert!(matches!(
1126 this_type.as_ref().unwrap().types[0],
1127 Atomic::TString
1128 ));
1129 }
1130
1131 #[test]
1132 fn substitute_conditional_all_branches() {
1133 let ty = Union::single(Atomic::TConditional {
1134 subject: Box::new(t_param("T")),
1135 if_true: Box::new(t_param("T")),
1136 if_false: Box::new(Union::single(Atomic::TInt)),
1137 });
1138 let result = ty.substitute_templates(&bindings_t_string());
1139 let Atomic::TConditional {
1140 subject,
1141 if_true,
1142 if_false,
1143 } = &result.types[0]
1144 else {
1145 panic!("expected TConditional");
1146 };
1147 assert!(matches!(subject.types[0], Atomic::TString));
1148 assert!(matches!(if_true.types[0], Atomic::TString));
1149 assert!(matches!(if_false.types[0], Atomic::TInt));
1150 }
1151
1152 #[test]
1153 fn substitute_intersection_parts() {
1154 let ty = Union::single(Atomic::TIntersection {
1155 parts: vec![
1156 Union::single(Atomic::TNamedObject {
1157 fqcn: Arc::from("Countable"),
1158 type_params: vec![],
1159 }),
1160 t_param("T"),
1161 ],
1162 });
1163 let result = ty.substitute_templates(&bindings_t_string());
1164 let Atomic::TIntersection { parts } = &result.types[0] else {
1165 panic!("expected TIntersection");
1166 };
1167 assert_eq!(parts.len(), 2);
1168 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1169 assert!(matches!(parts[1].types[0], Atomic::TString));
1170 }
1171
1172 #[test]
1173 fn substitute_no_template_params_identity() {
1174 let ty = Union::single(Atomic::TInt);
1175 let result = ty.substitute_templates(&bindings_t_string());
1176 assert!(matches!(result.types[0], Atomic::TInt));
1177 }
1178}