1use rustc_hash::FxHashMap;
2use serde::{Deserialize, Serialize};
3use smallvec::SmallVec;
4use std::sync::{Arc, OnceLock};
5
6use crate::atomic::Atomic;
7use crate::symbol::Name;
8
9pub fn empty_type_params() -> Arc<[Type]> {
13 static EMPTY: OnceLock<Arc<[Type]>> = OnceLock::new();
14 EMPTY.get_or_init(|| Arc::from([] as [Type; 0])).clone()
15}
16
17pub fn vec_to_type_params(v: Vec<Type>) -> Arc<[Type]> {
20 if v.is_empty() {
21 empty_type_params()
22 } else {
23 Arc::from(v)
24 }
25}
26
27pub type AtomicVec = SmallVec<[Atomic; 2]>;
29
30#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct Type {
36 pub types: AtomicVec,
37 pub possibly_undefined: bool,
39 pub from_docblock: bool,
41}
42
43impl Type {
44 pub fn empty() -> Self {
47 Self {
48 types: SmallVec::new(),
49 possibly_undefined: false,
50 from_docblock: false,
51 }
52 }
53
54 pub fn single(atomic: Atomic) -> Self {
55 let mut types = SmallVec::new();
56 types.push(atomic);
57 Self {
58 types,
59 possibly_undefined: false,
60 from_docblock: false,
61 }
62 }
63
64 pub fn mixed() -> Self {
65 Self::single(Atomic::TMixed)
66 }
67
68 pub fn void() -> Self {
69 Self::single(Atomic::TVoid)
70 }
71
72 pub fn never() -> Self {
73 Self::single(Atomic::TNever)
74 }
75
76 pub fn null() -> Self {
77 Self::single(Atomic::TNull)
78 }
79
80 pub fn bool() -> Self {
81 Self::single(Atomic::TBool)
82 }
83
84 pub fn int() -> Self {
85 Self::single(Atomic::TInt)
86 }
87
88 pub fn float() -> Self {
89 Self::single(Atomic::TFloat)
90 }
91
92 pub fn string() -> Self {
93 Self::single(Atomic::TString)
94 }
95
96 pub fn nullable(atomic: Atomic) -> Self {
98 let mut types = SmallVec::new();
99 types.push(atomic);
100 types.push(Atomic::TNull);
101 Self {
102 types,
103 possibly_undefined: false,
104 from_docblock: false,
105 }
106 }
107
108 pub fn from_vec(atomics: Vec<Atomic>) -> Self {
110 let mut u = Self::empty();
111 for a in atomics {
112 u.add_type(a);
113 }
114 u
115 }
116
117 pub fn is_empty(&self) -> bool {
120 self.types.is_empty()
121 }
122
123 pub fn is_single(&self) -> bool {
124 self.types.len() == 1
125 }
126
127 pub fn is_nullable(&self) -> bool {
128 self.types.iter().any(|t| matches!(t, Atomic::TNull))
129 }
130
131 pub fn is_mixed(&self) -> bool {
132 self.types.iter().any(|t| match t {
133 Atomic::TMixed => true,
134 Atomic::TTemplateParam { as_type, .. } => as_type.is_mixed(),
135 _ => false,
136 })
137 }
138
139 pub fn is_never(&self) -> bool {
140 self.types.iter().all(|t| matches!(t, Atomic::TNever)) && !self.types.is_empty()
141 }
142
143 pub fn is_void(&self) -> bool {
144 self.is_single() && matches!(self.types[0], Atomic::TVoid)
145 }
146
147 pub fn can_be_falsy(&self) -> bool {
148 self.types.iter().any(|t| t.can_be_falsy())
149 }
150
151 pub fn can_be_truthy(&self) -> bool {
152 self.types.iter().any(|t| t.can_be_truthy())
153 }
154
155 pub fn contains<F: Fn(&Atomic) -> bool>(&self, f: F) -> bool {
156 self.types.iter().any(f)
157 }
158
159 pub fn has_named_object(&self, fqcn: &str) -> bool {
160 self.types.iter().any(|t| match t {
161 Atomic::TNamedObject { fqcn: f, .. } => f.as_ref() == fqcn,
162 _ => false,
163 })
164 }
165
166 pub fn add_type(&mut self, atomic: Atomic) {
171 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
173 return;
174 }
175
176 if matches!(atomic, Atomic::TMixed) {
178 self.types.clear();
179 self.types.push(Atomic::TMixed);
180 return;
181 }
182
183 let atomic = if let Atomic::TConditional {
186 param_name: _,
187 subject: _,
188 if_true,
189 if_false,
190 } = &atomic
191 {
192 let mut simplified_true = Type::empty();
193 for t in &if_true.types {
194 simplified_true.add_type(t.clone());
195 }
196 let mut simplified_false = Type::empty();
197 for t in &if_false.types {
198 simplified_false.add_type(t.clone());
199 }
200 if simplified_true == simplified_false {
201 for t in simplified_true.types {
202 self.add_type(t);
203 }
204 return;
205 }
206 atomic
207 } else {
208 atomic
209 };
210
211 if self.types.contains(&atomic) {
213 return;
214 }
215
216 if let Atomic::TLiteralInt(_) = &atomic {
218 if self.types.iter().any(|t| matches!(t, Atomic::TInt)) {
219 return;
220 }
221 }
222 if let Atomic::TLiteralString(_) = &atomic {
224 if self.types.iter().any(|t| matches!(t, Atomic::TString)) {
225 return;
226 }
227 }
228 if matches!(atomic, Atomic::TTrue | Atomic::TFalse)
230 && self.types.iter().any(|t| matches!(t, Atomic::TBool))
231 {
232 return;
233 }
234 if matches!(atomic, Atomic::TInt) {
236 self.types.retain(|t| !matches!(t, Atomic::TLiteralInt(_)));
237 }
238 if matches!(atomic, Atomic::TString) {
240 self.types
241 .retain(|t| !matches!(t, Atomic::TLiteralString(_)));
242 }
243 if matches!(atomic, Atomic::TBool) {
245 self.types
246 .retain(|t| !matches!(t, Atomic::TTrue | Atomic::TFalse));
247 }
248
249 if matches!(atomic, Atomic::TNever) {
251 if !self.types.is_empty() {
252 return;
253 }
254 } else {
255 self.types.retain(|t| !matches!(t, Atomic::TNever));
256 }
257
258 if let Atomic::TKeyedArray { properties, .. } = &atomic {
261 if properties.is_empty() {
262 for existing in &self.types {
263 match existing {
264 Atomic::TArray { .. }
265 | Atomic::TNonEmptyArray { .. }
266 | Atomic::TList { .. }
267 | Atomic::TNonEmptyList { .. } => {
268 return; }
270 _ => {}
271 }
272 }
273 }
274 }
275
276 let is_generic_array_or_list = matches!(
278 &atomic,
279 Atomic::TArray { .. }
280 | Atomic::TNonEmptyArray { .. }
281 | Atomic::TList { .. }
282 | Atomic::TNonEmptyList { .. }
283 );
284 if is_generic_array_or_list {
285 self.types.retain(|t| {
286 if let Atomic::TKeyedArray { properties, .. } = t {
287 !properties.is_empty()
288 } else {
289 true
290 }
291 });
292 }
293
294 self.types.push(atomic);
295 }
296
297 pub fn remove_null(&self) -> Type {
301 self.filter(|t| !matches!(t, Atomic::TNull))
302 }
303
304 pub fn remove_false(&self) -> Type {
306 self.filter(|t| !matches!(t, Atomic::TFalse | Atomic::TBool))
307 }
308
309 pub fn core_type(&self) -> Type {
311 self.remove_null().remove_false()
312 }
313
314 pub fn narrow_to_truthy(&self) -> Type {
316 if self.is_mixed() {
317 return Type::mixed();
318 }
319 let narrowed = self.filter(|t| t.can_be_truthy());
320 narrowed.filter(|t| match t {
322 Atomic::TLiteralInt(0) => false,
323 Atomic::TLiteralString(s) if s.as_ref() == "" || s.as_ref() == "0" => false,
324 Atomic::TLiteralFloat(0, 0) => false,
325 _ => true,
326 })
327 }
328
329 pub fn narrow_to_falsy(&self) -> Type {
331 if self.is_mixed() {
332 return Type::from_vec(vec![
333 Atomic::TNull,
334 Atomic::TFalse,
335 Atomic::TLiteralInt(0),
336 Atomic::TLiteralString("".into()),
337 ]);
338 }
339 self.filter(|t| t.can_be_falsy())
340 }
341
342 pub fn narrow_instanceof(&self, class: &str) -> Type {
348 let narrowed_ty = Atomic::TNamedObject {
349 fqcn: class.into(),
350 type_params: empty_type_params(),
351 };
352 let has_object = self.types.iter().any(|t| {
354 matches!(
355 t,
356 Atomic::TObject | Atomic::TNamedObject { .. } | Atomic::TMixed | Atomic::TNull )
358 });
359 if has_object || self.is_empty() {
360 Type::single(narrowed_ty)
361 } else {
362 Type::single(narrowed_ty)
365 }
366 }
367
368 pub fn narrow_to_string(&self) -> Type {
370 self.filter(|t| t.is_string() || matches!(t, Atomic::TMixed | Atomic::TScalar))
371 }
372
373 pub fn narrow_to_int(&self) -> Type {
375 self.filter(|t| {
376 t.is_int() || matches!(t, Atomic::TMixed | Atomic::TScalar | Atomic::TNumeric)
377 })
378 }
379
380 pub fn narrow_to_float(&self) -> Type {
382 self.filter(|t| {
383 matches!(
384 t,
385 Atomic::TFloat
386 | Atomic::TLiteralFloat(..)
387 | Atomic::TMixed
388 | Atomic::TScalar
389 | Atomic::TNumeric
390 )
391 })
392 }
393
394 pub fn narrow_to_bool(&self) -> Type {
396 self.filter(|t| {
397 matches!(
398 t,
399 Atomic::TBool | Atomic::TTrue | Atomic::TFalse | Atomic::TMixed | Atomic::TScalar
400 )
401 })
402 }
403
404 pub fn narrow_to_null(&self) -> Type {
406 self.filter(|t| matches!(t, Atomic::TNull | Atomic::TMixed))
407 }
408
409 pub fn narrow_to_array(&self) -> Type {
411 self.filter(|t| t.is_array() || matches!(t, Atomic::TMixed))
412 }
413
414 pub fn narrow_to_object(&self) -> Type {
416 self.filter(|t| t.is_object() || matches!(t, Atomic::TMixed))
417 }
418
419 pub fn narrow_to_callable(&self) -> Type {
421 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
422 }
423
424 pub fn narrow_to_scalar(&self) -> Type {
426 self.filter(|t| {
427 matches!(
428 t,
429 Atomic::TString
430 | Atomic::TLiteralString(..)
431 | Atomic::TNumericString
432 | Atomic::TInt
433 | Atomic::TLiteralInt(..)
434 | Atomic::TFloat
435 | Atomic::TLiteralFloat(..)
436 | Atomic::TBool
437 | Atomic::TTrue
438 | Atomic::TFalse
439 | Atomic::TScalar
440 | Atomic::TMixed
441 )
442 })
443 }
444
445 pub fn narrow_to_iterable(&self) -> Type {
448 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
449 }
450
451 pub fn narrow_to_countable(&self) -> Type {
454 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
455 }
456
457 pub fn narrow_to_resource(&self) -> Type {
461 self.filter(|t| matches!(t, Atomic::TMixed))
463 }
464
465 pub fn merge(a: &Type, b: &Type) -> Type {
470 if b.types.is_empty() {
472 let mut result = a.clone();
473 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
474 return result;
475 }
476 if a.types.is_empty() {
478 let mut result = b.clone();
479 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
480 return result;
481 }
482 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
484 let mut result = a.clone();
485 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
486 return result;
487 }
488 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
490 return Type {
491 types: smallvec::smallvec![Atomic::TMixed],
492 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
493 from_docblock: a.from_docblock || b.from_docblock,
494 };
495 }
496 let mut result = a.clone();
497 result.merge_with(b);
498 result
499 }
500
501 pub fn merge_with(&mut self, other: &Type) {
503 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
504 self.possibly_undefined |= other.possibly_undefined;
505 return;
506 }
507 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
508 self.types.clear();
509 self.types.push(Atomic::TMixed);
510 self.possibly_undefined |= other.possibly_undefined;
511 return;
512 }
513 for atomic in &other.types {
514 self.add_type(atomic.clone());
515 }
516 self.possibly_undefined |= other.possibly_undefined;
517 }
518
519 pub fn intersect_with(&self, other: &Type) -> Type {
523 if self.is_mixed() {
524 return other.clone();
525 }
526 if other.is_mixed() {
527 return self.clone();
528 }
529 let mut result = Type::empty();
531 for a in &self.types {
532 for b in &other.types {
533 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
534 result.add_type(a.clone());
535 break;
536 }
537 }
538 }
539 if result.is_empty() {
540 Type::never()
541 } else {
542 result
543 }
544 }
545
546 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
550 if bindings.is_empty() {
551 return self.clone();
552 }
553 let mut result = Type::empty();
554 result.possibly_undefined = self.possibly_undefined;
555 result.from_docblock = self.from_docblock;
556 for atomic in &self.types {
557 match atomic {
558 Atomic::TTemplateParam { name, .. } => {
559 if let Some(resolved) = bindings.get(name) {
560 for t in &resolved.types {
561 result.add_type(t.clone());
562 }
563 } else {
564 result.add_type(atomic.clone());
565 }
566 }
567 Atomic::TArray { key, value } => {
568 result.add_type(Atomic::TArray {
569 key: Box::new(key.substitute_templates(bindings)),
570 value: Box::new(value.substitute_templates(bindings)),
571 });
572 }
573 Atomic::TList { value } => {
574 result.add_type(Atomic::TList {
575 value: Box::new(value.substitute_templates(bindings)),
576 });
577 }
578 Atomic::TNonEmptyArray { key, value } => {
579 result.add_type(Atomic::TNonEmptyArray {
580 key: Box::new(key.substitute_templates(bindings)),
581 value: Box::new(value.substitute_templates(bindings)),
582 });
583 }
584 Atomic::TNonEmptyList { value } => {
585 result.add_type(Atomic::TNonEmptyList {
586 value: Box::new(value.substitute_templates(bindings)),
587 });
588 }
589 Atomic::TKeyedArray {
590 properties,
591 is_open,
592 is_list,
593 } => {
594 use crate::atomic::KeyedProperty;
595 let new_props = properties
596 .iter()
597 .map(|(k, prop)| {
598 (
599 k.clone(),
600 KeyedProperty {
601 ty: prop.ty.substitute_templates(bindings),
602 optional: prop.optional,
603 },
604 )
605 })
606 .collect();
607 result.add_type(Atomic::TKeyedArray {
608 properties: new_props,
609 is_open: *is_open,
610 is_list: *is_list,
611 });
612 }
613 Atomic::TCallable {
614 params,
615 return_type,
616 } => {
617 result.add_type(Atomic::TCallable {
618 params: params.as_ref().map(|ps| {
619 ps.iter()
620 .map(|p| substitute_in_fn_param(p, bindings))
621 .collect()
622 }),
623 return_type: return_type
624 .as_ref()
625 .map(|r| Box::new(r.substitute_templates(bindings))),
626 });
627 }
628 Atomic::TClosure {
629 params,
630 return_type,
631 this_type,
632 } => {
633 result.add_type(Atomic::TClosure {
634 params: params
635 .iter()
636 .map(|p| substitute_in_fn_param(p, bindings))
637 .collect(),
638 return_type: Box::new(return_type.substitute_templates(bindings)),
639 this_type: this_type
640 .as_ref()
641 .map(|t| Box::new(t.substitute_templates(bindings))),
642 });
643 }
644 Atomic::TConditional {
645 param_name,
646 subject,
647 if_true,
648 if_false,
649 } => {
650 let new_subject = subject.substitute_templates(bindings);
651 let new_if_true = if_true.substitute_templates(bindings);
652 let new_if_false = if_false.substitute_templates(bindings);
653
654 let resolved = if let Some(name) = param_name {
658 if let Some(bound) = bindings.get(name) {
659 if new_subject.types.len() == 1 {
660 resolve_conditional_branch(
661 &new_subject.types[0],
662 bound,
663 &new_if_true,
664 &new_if_false,
665 )
666 } else {
667 None
668 }
669 } else {
670 None
671 }
672 } else {
673 None
674 };
675
676 if let Some(branch) = resolved {
677 for t in branch.types {
678 result.add_type(t);
679 }
680 } else {
681 result.add_type(Atomic::TConditional {
682 param_name: *param_name,
683 subject: Box::new(new_subject),
684 if_true: Box::new(new_if_true),
685 if_false: Box::new(new_if_false),
686 });
687 }
688 }
689 Atomic::TIntersection { parts } => {
690 result.add_type(Atomic::TIntersection {
691 parts: vec_to_type_params(
692 parts
693 .iter()
694 .map(|p| p.substitute_templates(bindings))
695 .collect(),
696 ),
697 });
698 }
699 Atomic::TNamedObject { fqcn, type_params } => {
700 if type_params.is_empty() && !fqcn.contains('\\') {
707 if let Some(resolved) = bindings.get(fqcn) {
708 for t in &resolved.types {
709 result.add_type(t.clone());
710 }
711 continue;
712 }
713 }
714 let new_params: Vec<Type> = type_params
715 .iter()
716 .map(|p| p.substitute_templates(bindings))
717 .collect();
718 result.add_type(Atomic::TNamedObject {
719 fqcn: *fqcn,
720 type_params: vec_to_type_params(new_params),
721 });
722 }
723 Atomic::TClassString(Some(param_name)) => {
725 if let Some(resolved) = bindings.get(param_name) {
726 for r_atomic in &resolved.types {
727 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
728 Some(*fqcn)
729 } else {
730 None
731 };
732 result.add_type(Atomic::TClassString(cls_name));
733 }
734 } else {
735 result.add_type(atomic.clone());
736 }
737 }
738 _ => {
739 result.add_type(atomic.clone());
740 }
741 }
742 }
743 result
744 }
745
746 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
752 where
753 F: Fn(&str) -> Option<Type>,
754 {
755 self.resolve_conditional_inner(&lookup)
756 }
757
758 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
759 where
760 F: Fn(&str) -> Option<Type>,
761 {
762 let mut result = Type::empty();
763 for atomic in self.types {
764 match atomic {
765 Atomic::TConditional {
766 ref param_name,
767 ref subject,
768 ref if_true,
769 ref if_false,
770 } => {
771 let resolved = if subject.types.len() == 1 {
772 if let Some(name) = param_name {
773 if let Some(arg_ty) = lookup(name.as_ref()) {
774 resolve_conditional_branch(
775 &subject.types[0],
776 &arg_ty,
777 if_true,
778 if_false,
779 )
780 } else {
781 None
782 }
783 } else {
784 None
785 }
786 } else {
787 None
788 };
789
790 if let Some(branch) = resolved {
791 for t in branch.resolve_conditional_inner(lookup).types {
793 result.add_type(t);
794 }
795 } else {
796 for t in if_true.clone().resolve_conditional_inner(lookup).types {
799 result.add_type(t);
800 }
801 for t in if_false.clone().resolve_conditional_inner(lookup).types {
802 result.add_type(t);
803 }
804 }
805 }
806 other => result.add_type(other),
807 }
808 }
809 result
810 }
811
812 pub fn is_subtype_structural(&self, other: &Type) -> bool {
822 if other.is_mixed() {
823 return true;
824 }
825 if self.is_never() {
826 return true; }
828 self.types
829 .iter()
830 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
831 }
832
833 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
836 let mut result = Type::empty();
837 result.possibly_undefined = self.possibly_undefined;
838 result.from_docblock = self.from_docblock;
839 for atomic in &self.types {
840 if f(atomic) {
841 result.types.push(atomic.clone());
842 }
843 }
844 result
845 }
846
847 pub fn possibly_undefined(mut self) -> Self {
849 self.possibly_undefined = true;
850 self
851 }
852
853 pub fn from_docblock(mut self) -> Self {
855 self.from_docblock = true;
856 self
857 }
858}
859
860fn is_string_atomic(a: &Atomic) -> bool {
865 matches!(
866 a,
867 Atomic::TString
868 | Atomic::TNonEmptyString
869 | Atomic::TLiteralString(_)
870 | Atomic::TNumericString
871 | Atomic::TClassString(_)
872 | Atomic::TCallableString
873 )
874}
875
876fn is_array_atomic(a: &Atomic) -> bool {
877 matches!(
878 a,
879 Atomic::TArray { .. }
880 | Atomic::TNonEmptyArray { .. }
881 | Atomic::TKeyedArray { .. }
882 | Atomic::TList { .. }
883 | Atomic::TNonEmptyList { .. }
884 )
885}
886
887fn is_list_atomic(a: &Atomic) -> bool {
888 match a {
889 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
890 Atomic::TKeyedArray { is_list, .. } => *is_list,
891 _ => false,
892 }
893}
894
895fn resolve_conditional_branch(
901 subject: &Atomic,
902 arg_ty: &Type,
903 if_true: &Type,
904 if_false: &Type,
905) -> Option<Type> {
906 let predicate: fn(&Atomic) -> bool = match subject {
907 Atomic::TNull => |a| matches!(a, Atomic::TNull),
908 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
909 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
910 Atomic::TString => is_string_atomic,
911 Atomic::TList { .. } => is_list_atomic,
912 Atomic::TArray { .. } => is_array_atomic,
913 _ => return None,
914 };
915
916 if arg_ty.types.is_empty() {
917 return None;
918 }
919 let all_match = arg_ty.types.iter().all(&predicate);
920 let none_match = !arg_ty.types.iter().any(predicate);
921 if all_match {
922 Some(if_true.clone())
923 } else if none_match {
924 Some(if_false.clone())
925 } else {
926 None
927 }
928}
929
930fn substitute_in_fn_param(
935 p: &crate::atomic::FnParam,
936 bindings: &FxHashMap<Name, Type>,
937) -> crate::atomic::FnParam {
938 crate::atomic::FnParam {
939 name: p.name,
940 ty: p.ty.as_ref().map(|t| {
941 let u = t.to_union();
942 let substituted = u.substitute_templates(bindings);
943 crate::compact::SimpleType::from_union(substituted)
944 }),
945 default: p.default.as_ref().map(|d| {
946 let u = d.to_union();
947 let substituted = u.substitute_templates(bindings);
948 crate::compact::SimpleType::from_union(substituted)
949 }),
950 is_variadic: p.is_variadic,
951 is_byref: p.is_byref,
952 is_optional: p.is_optional,
953 }
954}
955
956fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
961 if sub == sup {
962 return true;
963 }
964 match (sub, sup) {
965 (Atomic::TNever, _) => true,
967 (_, Atomic::TMixed) => true,
969 (Atomic::TMixed, _) => true,
970 (_, Atomic::TTemplateParam { as_type, .. }) => {
975 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
976 }
977
978 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
980 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
981 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
982 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
983 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
984 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
985 (Atomic::TPositiveInt, Atomic::TInt) => true,
986 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
987 (Atomic::TNegativeInt, Atomic::TInt) => true,
988 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
989 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
990
991 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
992 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
993 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
994
995 (Atomic::TLiteralString(s), Atomic::TString) => {
996 let _ = s;
997 true
998 }
999 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1000 let _ = s;
1001 true
1002 }
1003 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1004 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1005 (Atomic::TNonEmptyString, Atomic::TString) => true,
1006 (Atomic::TCallableString, Atomic::TString) => true,
1007 (Atomic::TNumericString, Atomic::TString) => true,
1008 (Atomic::TClassString(_), Atomic::TString) => true,
1009 (Atomic::TInterfaceString, Atomic::TString) => true,
1010 (Atomic::TEnumString, Atomic::TString) => true,
1011 (Atomic::TTraitString, Atomic::TString) => true,
1012
1013 (Atomic::TTrue, Atomic::TBool) => true,
1014 (Atomic::TFalse, Atomic::TBool) => true,
1015
1016 (Atomic::TInt, Atomic::TNumeric) => true,
1017 (Atomic::TFloat, Atomic::TNumeric) => true,
1018 (Atomic::TNumericString, Atomic::TNumeric) => true,
1019
1020 (Atomic::TInt, Atomic::TScalar) => true,
1021 (Atomic::TFloat, Atomic::TScalar) => true,
1022 (Atomic::TString, Atomic::TScalar) => true,
1023 (Atomic::TBool, Atomic::TScalar) => true,
1024 (Atomic::TNumeric, Atomic::TScalar) => true,
1025 (Atomic::TTrue, Atomic::TScalar) => true,
1026 (Atomic::TFalse, Atomic::TScalar) => true,
1027
1028 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1030 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1031 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1032 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1034 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1035 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1037 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1038 (
1042 Atomic::TNamedObject {
1043 fqcn: sub_fqcn,
1044 type_params: sub_params,
1045 },
1046 Atomic::TNamedObject {
1047 fqcn: sup_fqcn,
1048 type_params: sup_params,
1049 },
1050 ) => sub_fqcn == sup_fqcn && (sup_params.is_empty() || sub_params == sup_params),
1051
1052 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1054 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1055 (Atomic::TInt, Atomic::TFloat) => true,
1056
1057 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
1059
1060 (Atomic::TString, Atomic::TCallable { .. }) => true,
1062 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1063 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1064 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1065 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1066
1067 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1069 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1071 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1073 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1075 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1077 fqcn.as_ref().eq_ignore_ascii_case("closure")
1078 }
1079 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1080 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1082 fqcn.as_ref().eq_ignore_ascii_case("closure")
1083 }
1084
1085 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1087 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1089 && value.is_subtype_structural(av)
1090 }
1091 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1092 value.is_subtype_structural(lv)
1093 }
1094 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1096 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1097 && av.is_subtype_structural(lv)
1098 }
1099 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1100 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1101 && av.is_subtype_structural(lv)
1102 }
1103 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1104 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1105 && av.is_subtype_structural(lv)
1106 }
1107 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1108 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1109 && av.is_subtype_structural(lv)
1110 }
1111 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1113 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1114 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1115 }
1116
1117 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1119 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1120 }
1121
1122 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
1124
1125 (
1127 Atomic::TKeyedArray {
1128 properties,
1129 is_list,
1130 ..
1131 },
1132 Atomic::TList { value: lv },
1133 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1134 (
1135 Atomic::TKeyedArray {
1136 properties,
1137 is_list,
1138 ..
1139 },
1140 Atomic::TNonEmptyList { value: lv },
1141 ) => {
1142 *is_list
1143 && !properties.is_empty()
1144 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1145 }
1146
1147 _ => false,
1148 }
1149}
1150
1151#[cfg(test)]
1156mod tests {
1157 use std::sync::Arc;
1158
1159 use super::*;
1160
1161 #[test]
1162 fn single_is_single() {
1163 let u = Type::single(Atomic::TString);
1164 assert!(u.is_single());
1165 assert!(!u.is_nullable());
1166 }
1167
1168 #[test]
1169 fn nullable_has_null() {
1170 let u = Type::nullable(Atomic::TString);
1171 assert!(u.is_nullable());
1172 assert_eq!(u.types.len(), 2);
1173 }
1174
1175 #[test]
1176 fn add_type_deduplicates() {
1177 let mut u = Type::single(Atomic::TString);
1178 u.add_type(Atomic::TString);
1179 assert_eq!(u.types.len(), 1);
1180 }
1181
1182 #[test]
1183 fn add_type_literal_subsumed_by_base() {
1184 let mut u = Type::single(Atomic::TInt);
1185 u.add_type(Atomic::TLiteralInt(42));
1186 assert_eq!(u.types.len(), 1);
1187 assert!(matches!(u.types[0], Atomic::TInt));
1188 }
1189
1190 #[test]
1191 fn add_type_base_widens_literals() {
1192 let mut u = Type::single(Atomic::TLiteralInt(1));
1193 u.add_type(Atomic::TLiteralInt(2));
1194 u.add_type(Atomic::TInt);
1195 assert_eq!(u.types.len(), 1);
1196 assert!(matches!(u.types[0], Atomic::TInt));
1197 }
1198
1199 #[test]
1200 fn mixed_subsumes_everything() {
1201 let mut u = Type::single(Atomic::TString);
1202 u.add_type(Atomic::TMixed);
1203 assert_eq!(u.types.len(), 1);
1204 assert!(u.is_mixed());
1205 }
1206
1207 #[test]
1208 fn remove_null() {
1209 let u = Type::nullable(Atomic::TString);
1210 let narrowed = u.remove_null();
1211 assert!(!narrowed.is_nullable());
1212 assert_eq!(narrowed.types.len(), 1);
1213 }
1214
1215 #[test]
1216 fn narrow_to_truthy_removes_null_false() {
1217 let mut u = Type::empty();
1218 u.add_type(Atomic::TString);
1219 u.add_type(Atomic::TNull);
1220 u.add_type(Atomic::TFalse);
1221 let truthy = u.narrow_to_truthy();
1222 assert!(!truthy.is_nullable());
1223 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1224 }
1225
1226 #[test]
1227 fn merge_combines_types() {
1228 let a = Type::single(Atomic::TString);
1229 let b = Type::single(Atomic::TInt);
1230 let merged = Type::merge(&a, &b);
1231 assert_eq!(merged.types.len(), 2);
1232 }
1233
1234 #[test]
1235 fn subtype_literal_int_under_int() {
1236 let sub = Type::single(Atomic::TLiteralInt(5));
1237 let sup = Type::single(Atomic::TInt);
1238 assert!(sub.is_subtype_structural(&sup));
1239 }
1240
1241 #[test]
1242 fn subtype_never_is_bottom() {
1243 let never = Type::never();
1244 let string = Type::single(Atomic::TString);
1245 assert!(never.is_subtype_structural(&string));
1246 }
1247
1248 #[test]
1249 fn subtype_everything_under_mixed() {
1250 let string = Type::single(Atomic::TString);
1251 let mixed = Type::mixed();
1252 assert!(string.is_subtype_structural(&mixed));
1253 }
1254
1255 #[test]
1256 fn template_substitution() {
1257 let mut bindings = FxHashMap::default();
1258 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1259
1260 let tmpl = Type::single(Atomic::TTemplateParam {
1261 name: Name::new("T"),
1262 as_type: Box::new(Type::mixed()),
1263 defining_entity: Name::new("MyClass"),
1264 });
1265
1266 let resolved = tmpl.substitute_templates(&bindings);
1267 assert_eq!(resolved.types.len(), 1);
1268 assert!(matches!(resolved.types[0], Atomic::TString));
1269 }
1270
1271 #[test]
1272 fn intersection_is_object() {
1273 let parts = vec![
1274 Type::single(Atomic::TNamedObject {
1275 fqcn: Name::new("Iterator"),
1276 type_params: empty_type_params(),
1277 }),
1278 Type::single(Atomic::TNamedObject {
1279 fqcn: Name::new("Countable"),
1280 type_params: empty_type_params(),
1281 }),
1282 ];
1283 let atomic = Atomic::TIntersection {
1284 parts: vec_to_type_params(parts),
1285 };
1286 assert!(atomic.is_object());
1287 assert!(!atomic.can_be_falsy());
1288 assert!(atomic.can_be_truthy());
1289 }
1290
1291 #[test]
1292 fn intersection_display_two_parts() {
1293 let parts = vec![
1294 Type::single(Atomic::TNamedObject {
1295 fqcn: Name::new("Iterator"),
1296 type_params: empty_type_params(),
1297 }),
1298 Type::single(Atomic::TNamedObject {
1299 fqcn: Name::new("Countable"),
1300 type_params: empty_type_params(),
1301 }),
1302 ];
1303 let u = Type::single(Atomic::TIntersection {
1304 parts: vec_to_type_params(parts),
1305 });
1306 assert_eq!(format!("{u}"), "Iterator&Countable");
1307 }
1308
1309 #[test]
1310 fn intersection_display_three_parts() {
1311 let parts = vec![
1312 Type::single(Atomic::TNamedObject {
1313 fqcn: Name::new("A"),
1314 type_params: empty_type_params(),
1315 }),
1316 Type::single(Atomic::TNamedObject {
1317 fqcn: Name::new("B"),
1318 type_params: empty_type_params(),
1319 }),
1320 Type::single(Atomic::TNamedObject {
1321 fqcn: Name::new("C"),
1322 type_params: empty_type_params(),
1323 }),
1324 ];
1325 let u = Type::single(Atomic::TIntersection {
1326 parts: vec_to_type_params(parts),
1327 });
1328 assert_eq!(format!("{u}"), "A&B&C");
1329 }
1330
1331 #[test]
1332 fn intersection_in_nullable_union_display() {
1333 let intersection = Atomic::TIntersection {
1334 parts: vec_to_type_params(vec![
1335 Type::single(Atomic::TNamedObject {
1336 fqcn: Name::new("Iterator"),
1337 type_params: empty_type_params(),
1338 }),
1339 Type::single(Atomic::TNamedObject {
1340 fqcn: Name::new("Countable"),
1341 type_params: empty_type_params(),
1342 }),
1343 ]),
1344 };
1345 let mut u = Type::single(intersection);
1346 u.add_type(Atomic::TNull);
1347 assert!(u.is_nullable());
1348 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1349 }
1350
1351 fn t_param(name: &str) -> Type {
1354 Type::single(Atomic::TTemplateParam {
1355 name: Name::new(name),
1356 as_type: Box::new(Type::mixed()),
1357 defining_entity: Name::new("Fn"),
1358 })
1359 }
1360
1361 fn bindings_t_string() -> FxHashMap<Name, Type> {
1362 let mut b = FxHashMap::default();
1363 b.insert(Name::new("T"), Type::single(Atomic::TString));
1364 b
1365 }
1366
1367 #[test]
1368 fn substitute_non_empty_array_key_and_value() {
1369 let ty = Type::single(Atomic::TNonEmptyArray {
1370 key: Box::new(t_param("T")),
1371 value: Box::new(t_param("T")),
1372 });
1373 let result = ty.substitute_templates(&bindings_t_string());
1374 assert_eq!(result.types.len(), 1);
1375 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1376 panic!("expected TNonEmptyArray");
1377 };
1378 assert!(matches!(key.types[0], Atomic::TString));
1379 assert!(matches!(value.types[0], Atomic::TString));
1380 }
1381
1382 #[test]
1383 fn substitute_non_empty_list_value() {
1384 let ty = Type::single(Atomic::TNonEmptyList {
1385 value: Box::new(t_param("T")),
1386 });
1387 let result = ty.substitute_templates(&bindings_t_string());
1388 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1389 panic!("expected TNonEmptyList");
1390 };
1391 assert!(matches!(value.types[0], Atomic::TString));
1392 }
1393
1394 #[test]
1395 fn substitute_keyed_array_property_types() {
1396 use crate::atomic::{ArrayKey, KeyedProperty};
1397 use indexmap::IndexMap;
1398 let mut props = IndexMap::new();
1399 props.insert(
1400 ArrayKey::String(Arc::from("name")),
1401 KeyedProperty {
1402 ty: t_param("T"),
1403 optional: false,
1404 },
1405 );
1406 props.insert(
1407 ArrayKey::String(Arc::from("tag")),
1408 KeyedProperty {
1409 ty: t_param("T"),
1410 optional: true,
1411 },
1412 );
1413 let ty = Type::single(Atomic::TKeyedArray {
1414 properties: props,
1415 is_open: true,
1416 is_list: false,
1417 });
1418 let result = ty.substitute_templates(&bindings_t_string());
1419 let Atomic::TKeyedArray {
1420 properties,
1421 is_open,
1422 is_list,
1423 } = &result.types[0]
1424 else {
1425 panic!("expected TKeyedArray");
1426 };
1427 assert!(is_open);
1428 assert!(!is_list);
1429 assert!(matches!(
1430 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1431 Atomic::TString
1432 ));
1433 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1434 assert!(matches!(
1435 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1436 Atomic::TString
1437 ));
1438 }
1439
1440 #[test]
1441 fn substitute_callable_params_and_return() {
1442 use crate::atomic::FnParam;
1443 let ty = Type::single(Atomic::TCallable {
1444 params: Some(vec![FnParam {
1445 name: Name::new("x"),
1446 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1447 default: None,
1448 is_variadic: false,
1449 is_byref: false,
1450 is_optional: false,
1451 }]),
1452 return_type: Some(Box::new(t_param("T"))),
1453 });
1454 let result = ty.substitute_templates(&bindings_t_string());
1455 let Atomic::TCallable {
1456 params,
1457 return_type,
1458 } = &result.types[0]
1459 else {
1460 panic!("expected TCallable");
1461 };
1462 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1463 let param_union = param_ty.to_union();
1464 assert!(matches!(param_union.types[0], Atomic::TString));
1465 let ret = return_type.as_ref().unwrap();
1466 assert!(matches!(ret.types[0], Atomic::TString));
1467 }
1468
1469 #[test]
1470 fn substitute_callable_bare_no_panic() {
1471 let ty = Type::single(Atomic::TCallable {
1473 params: None,
1474 return_type: None,
1475 });
1476 let result = ty.substitute_templates(&bindings_t_string());
1477 assert!(matches!(
1478 result.types[0],
1479 Atomic::TCallable {
1480 params: None,
1481 return_type: None
1482 }
1483 ));
1484 }
1485
1486 #[test]
1487 fn substitute_closure_params_return_and_this() {
1488 use crate::atomic::FnParam;
1489 let ty = Type::single(Atomic::TClosure {
1490 params: vec![FnParam {
1491 name: Name::new("a"),
1492 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1493 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1494 is_variadic: true,
1495 is_byref: true,
1496 is_optional: true,
1497 }],
1498 return_type: Box::new(t_param("T")),
1499 this_type: Some(Box::new(t_param("T"))),
1500 });
1501 let result = ty.substitute_templates(&bindings_t_string());
1502 let Atomic::TClosure {
1503 params,
1504 return_type,
1505 this_type,
1506 } = &result.types[0]
1507 else {
1508 panic!("expected TClosure");
1509 };
1510 let p = ¶ms[0];
1511 let ty_union = p.ty.as_ref().unwrap().to_union();
1512 let default_union = p.default.as_ref().unwrap().to_union();
1513 assert!(matches!(ty_union.types[0], Atomic::TString));
1514 assert!(matches!(default_union.types[0], Atomic::TString));
1515 assert!(p.is_variadic);
1517 assert!(p.is_byref);
1518 assert!(p.is_optional);
1519 assert!(matches!(return_type.types[0], Atomic::TString));
1520 assert!(matches!(
1521 this_type.as_ref().unwrap().types[0],
1522 Atomic::TString
1523 ));
1524 }
1525
1526 #[test]
1527 fn substitute_conditional_all_branches() {
1528 let ty = Type::single(Atomic::TConditional {
1529 param_name: None,
1530 subject: Box::new(t_param("T")),
1531 if_true: Box::new(t_param("T")),
1532 if_false: Box::new(Type::single(Atomic::TInt)),
1533 });
1534 let result = ty.substitute_templates(&bindings_t_string());
1535 let Atomic::TConditional {
1536 param_name: _,
1537 subject,
1538 if_true,
1539 if_false,
1540 } = &result.types[0]
1541 else {
1542 panic!("expected TConditional");
1543 };
1544 assert!(matches!(subject.types[0], Atomic::TString));
1545 assert!(matches!(if_true.types[0], Atomic::TString));
1546 assert!(matches!(if_false.types[0], Atomic::TInt));
1547 }
1548
1549 #[test]
1550 fn resolve_conditional_is_null_non_null_arg() {
1551 let ty = Type::single(Atomic::TConditional {
1552 param_name: Some(Name::new("x")),
1553 subject: Box::new(Type::single(Atomic::TNull)),
1554 if_true: Box::new(Type::single(Atomic::TInt)),
1555 if_false: Box::new(Type::single(Atomic::TString)),
1556 });
1557 let result = ty.resolve_conditional_returns(|name| {
1558 if name == "x" {
1559 Some(Type::single(Atomic::TString)) } else {
1561 None
1562 }
1563 });
1564 assert!(result.types.len() == 1);
1565 assert!(matches!(result.types[0], Atomic::TString));
1566 }
1567
1568 #[test]
1569 fn resolve_conditional_is_null_null_arg() {
1570 let ty = Type::single(Atomic::TConditional {
1571 param_name: Some(Name::new("x")),
1572 subject: Box::new(Type::single(Atomic::TNull)),
1573 if_true: Box::new(Type::single(Atomic::TInt)),
1574 if_false: Box::new(Type::single(Atomic::TString)),
1575 });
1576 let result = ty.resolve_conditional_returns(|name| {
1577 if name == "x" {
1578 Some(Type::single(Atomic::TNull)) } else {
1580 None
1581 }
1582 });
1583 assert!(result.types.len() == 1);
1584 assert!(matches!(result.types[0], Atomic::TInt));
1585 }
1586
1587 #[test]
1588 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1589 let mut nullable_str = Type::single(Atomic::TString);
1590 nullable_str.add_type(Atomic::TNull);
1591 let ty = Type::single(Atomic::TConditional {
1592 param_name: Some(Name::new("x")),
1593 subject: Box::new(Type::single(Atomic::TNull)),
1594 if_true: Box::new(Type::single(Atomic::TInt)),
1595 if_false: Box::new(Type::single(Atomic::TString)),
1596 });
1597 let result = ty.resolve_conditional_returns(|name| {
1598 if name == "x" {
1599 Some(nullable_str.clone())
1600 } else {
1601 None
1602 }
1603 });
1604 assert_eq!(result.types.len(), 2);
1606 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1607 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1608 }
1609
1610 #[test]
1611 fn resolve_conditional_nested_widens_inner_branch() {
1612 let inner = Type::single(Atomic::TConditional {
1615 param_name: Some(Name::new("x")),
1616 subject: Box::new(Type::single(Atomic::TString)),
1617 if_true: Box::new(Type::single(Atomic::TString)),
1618 if_false: Box::new(Type::single(Atomic::TFloat)),
1619 });
1620 let ty = Type::single(Atomic::TConditional {
1621 param_name: Some(Name::new("x")),
1622 subject: Box::new(Type::single(Atomic::TNull)),
1623 if_true: Box::new(Type::single(Atomic::TInt)),
1624 if_false: Box::new(inner),
1625 });
1626 let result = ty.resolve_conditional_returns(|_| None);
1628 assert!(
1629 result
1630 .types
1631 .iter()
1632 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1633 "no TConditional should survive: {:?}",
1634 result.types
1635 );
1636 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1637 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1638 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
1639 }
1640
1641 #[test]
1642 fn resolve_conditional_nested_resolves_inner_branch() {
1643 let inner = Type::single(Atomic::TConditional {
1647 param_name: Some(Name::new("x")),
1648 subject: Box::new(Type::single(Atomic::TString)),
1649 if_true: Box::new(Type::single(Atomic::TString)),
1650 if_false: Box::new(Type::single(Atomic::TFloat)),
1651 });
1652 let ty = Type::single(Atomic::TConditional {
1653 param_name: Some(Name::new("x")),
1654 subject: Box::new(Type::single(Atomic::TNull)),
1655 if_true: Box::new(Type::single(Atomic::TInt)),
1656 if_false: Box::new(inner),
1657 });
1658 let result = ty.resolve_conditional_returns(|name| {
1660 if name == "x" {
1661 Some(Type::single(Atomic::TString))
1662 } else {
1663 None
1664 }
1665 });
1666 assert!(
1667 result
1668 .types
1669 .iter()
1670 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1671 "no TConditional should survive: {:?}",
1672 result.types
1673 );
1674 assert_eq!(result.types.len(), 1);
1675 assert!(matches!(result.types[0], Atomic::TString));
1676 }
1677
1678 #[test]
1679 fn substitute_intersection_parts() {
1680 let ty = Type::single(Atomic::TIntersection {
1681 parts: vec_to_type_params(vec![
1682 Type::single(Atomic::TNamedObject {
1683 fqcn: Name::new("Countable"),
1684 type_params: empty_type_params(),
1685 }),
1686 t_param("T"),
1687 ]),
1688 });
1689 let result = ty.substitute_templates(&bindings_t_string());
1690 let Atomic::TIntersection { parts } = &result.types[0] else {
1691 panic!("expected TIntersection");
1692 };
1693 assert_eq!(parts.len(), 2);
1694 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1695 assert!(matches!(parts[1].types[0], Atomic::TString));
1696 }
1697
1698 #[test]
1699 fn substitute_no_template_params_identity() {
1700 let ty = Type::single(Atomic::TInt);
1701 let result = ty.substitute_templates(&bindings_t_string());
1702 assert!(matches!(result.types[0], Atomic::TInt));
1703 }
1704}