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