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 {
461 let mut out = Type::empty();
462 for t in &self.types {
463 if matches!(t, Atomic::TMixed) {
464 out.add_type(Atomic::TObject);
465 } else if t.is_object() {
466 out.add_type(t.clone());
467 }
468 }
469 if out.types.is_empty() {
470 self.filter(|t| t.is_object())
471 } else {
472 out
473 }
474 }
475
476 pub fn narrow_to_callable(&self) -> Type {
478 self.filter(|t| t.is_callable() || matches!(t, Atomic::TMixed))
479 }
480
481 pub fn narrow_to_scalar(&self) -> Type {
483 self.filter(|t| {
484 matches!(
485 t,
486 Atomic::TString
487 | Atomic::TLiteralString(..)
488 | Atomic::TNumericString
489 | Atomic::TInt
490 | Atomic::TLiteralInt(..)
491 | Atomic::TFloat
492 | Atomic::TLiteralFloat(..)
493 | Atomic::TBool
494 | Atomic::TTrue
495 | Atomic::TFalse
496 | Atomic::TScalar
497 | Atomic::TMixed
498 )
499 })
500 }
501
502 pub fn narrow_to_iterable(&self) -> Type {
505 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
506 }
507
508 pub fn narrow_to_countable(&self) -> Type {
511 self.filter(|t| t.is_array() || t.is_object() || matches!(t, Atomic::TMixed))
512 }
513
514 pub fn narrow_to_resource(&self) -> Type {
518 self.filter(|t| matches!(t, Atomic::TMixed))
520 }
521
522 pub fn merge(a: &Type, b: &Type) -> Type {
527 if b.types.is_empty() {
529 let mut result = a.clone();
530 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
531 return result;
532 }
533 if a.types.is_empty() {
535 let mut result = b.clone();
536 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
537 return result;
538 }
539 if a.types.len() == 1 && matches!(a.types[0], Atomic::TMixed) {
541 let mut result = a.clone();
542 result.possibly_undefined = a.possibly_undefined || b.possibly_undefined;
543 return result;
544 }
545 if b.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
547 return Type {
548 types: smallvec::smallvec![Atomic::TMixed],
549 possibly_undefined: a.possibly_undefined || b.possibly_undefined,
550 from_docblock: a.from_docblock || b.from_docblock,
551 };
552 }
553 let mut result = a.clone();
554 result.merge_with(b);
555 result
556 }
557
558 pub fn merge_with(&mut self, other: &Type) {
560 if self.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
561 self.possibly_undefined |= other.possibly_undefined;
562 return;
563 }
564 if other.types.iter().any(|t| matches!(t, Atomic::TMixed)) {
565 self.types.clear();
566 self.types.push(Atomic::TMixed);
567 self.possibly_undefined |= other.possibly_undefined;
568 return;
569 }
570 for atomic in &other.types {
571 self.add_type(atomic.clone());
572 }
573 self.possibly_undefined |= other.possibly_undefined;
574 }
575
576 pub fn intersect_with(&self, other: &Type) -> Type {
580 if self.is_mixed() {
581 return other.clone();
582 }
583 if other.is_mixed() {
584 return self.clone();
585 }
586 let mut result = Type::empty();
588 for a in &self.types {
589 for b in &other.types {
590 if a == b || atomic_subtype(a, b) || atomic_subtype(b, a) {
591 result.add_type(a.clone());
592 break;
593 }
594 }
595 }
596 if result.is_empty() {
597 Type::never()
598 } else {
599 result
600 }
601 }
602
603 pub fn substitute_templates(&self, bindings: &FxHashMap<Name, Type>) -> Type {
607 if bindings.is_empty() {
608 return self.clone();
609 }
610 let mut result = Type::empty();
611 result.possibly_undefined = self.possibly_undefined;
612 result.from_docblock = self.from_docblock;
613 for atomic in &self.types {
614 match atomic {
615 Atomic::TTemplateParam { name, .. } => {
616 if let Some(resolved) = bindings.get(name) {
617 for t in &resolved.types {
618 result.add_type(t.clone());
619 }
620 } else {
621 result.add_type(atomic.clone());
622 }
623 }
624 Atomic::TArray { key, value } => {
625 result.add_type(Atomic::TArray {
626 key: Box::new(key.substitute_templates(bindings)),
627 value: Box::new(value.substitute_templates(bindings)),
628 });
629 }
630 Atomic::TList { value } => {
631 result.add_type(Atomic::TList {
632 value: Box::new(value.substitute_templates(bindings)),
633 });
634 }
635 Atomic::TNonEmptyArray { key, value } => {
636 result.add_type(Atomic::TNonEmptyArray {
637 key: Box::new(key.substitute_templates(bindings)),
638 value: Box::new(value.substitute_templates(bindings)),
639 });
640 }
641 Atomic::TNonEmptyList { value } => {
642 result.add_type(Atomic::TNonEmptyList {
643 value: Box::new(value.substitute_templates(bindings)),
644 });
645 }
646 Atomic::TKeyedArray {
647 properties,
648 is_open,
649 is_list,
650 } => {
651 use crate::atomic::KeyedProperty;
652 let new_props = properties
653 .iter()
654 .map(|(k, prop)| {
655 (
656 k.clone(),
657 KeyedProperty {
658 ty: prop.ty.substitute_templates(bindings),
659 optional: prop.optional,
660 },
661 )
662 })
663 .collect();
664 result.add_type(Atomic::TKeyedArray {
665 properties: new_props,
666 is_open: *is_open,
667 is_list: *is_list,
668 });
669 }
670 Atomic::TCallable {
671 params,
672 return_type,
673 } => {
674 result.add_type(Atomic::TCallable {
675 params: params.as_ref().map(|ps| {
676 ps.iter()
677 .map(|p| substitute_in_fn_param(p, bindings))
678 .collect()
679 }),
680 return_type: return_type
681 .as_ref()
682 .map(|r| Box::new(r.substitute_templates(bindings))),
683 });
684 }
685 Atomic::TClosure {
686 params,
687 return_type,
688 this_type,
689 } => {
690 result.add_type(Atomic::TClosure {
691 params: params
692 .iter()
693 .map(|p| substitute_in_fn_param(p, bindings))
694 .collect(),
695 return_type: Box::new(return_type.substitute_templates(bindings)),
696 this_type: this_type
697 .as_ref()
698 .map(|t| Box::new(t.substitute_templates(bindings))),
699 });
700 }
701 Atomic::TConditional {
702 param_name,
703 subject,
704 if_true,
705 if_false,
706 } => {
707 let new_subject = subject.substitute_templates(bindings);
708 let new_if_true = if_true.substitute_templates(bindings);
709 let new_if_false = if_false.substitute_templates(bindings);
710
711 let resolved = if let Some(name) = param_name {
715 if let Some(bound) = bindings.get(name) {
716 if new_subject.types.len() == 1 {
717 resolve_conditional_branch(
718 &new_subject.types[0],
719 bound,
720 &new_if_true,
721 &new_if_false,
722 )
723 } else {
724 None
725 }
726 } else {
727 None
728 }
729 } else {
730 None
731 };
732
733 if let Some(branch) = resolved {
734 for t in branch.types {
735 result.add_type(t);
736 }
737 } else {
738 result.add_type(Atomic::TConditional {
739 param_name: *param_name,
740 subject: Box::new(new_subject),
741 if_true: Box::new(new_if_true),
742 if_false: Box::new(new_if_false),
743 });
744 }
745 }
746 Atomic::TIntersection { parts } => {
747 result.add_type(Atomic::TIntersection {
748 parts: vec_to_type_params(
749 parts
750 .iter()
751 .map(|p| p.substitute_templates(bindings))
752 .collect(),
753 ),
754 });
755 }
756 Atomic::TNamedObject { fqcn, type_params } => {
757 if type_params.is_empty() && !fqcn.contains('\\') {
764 if let Some(resolved) = bindings.get(fqcn) {
765 for t in &resolved.types {
766 result.add_type(t.clone());
767 }
768 continue;
769 }
770 }
771 let new_params: Vec<Type> = type_params
772 .iter()
773 .map(|p| p.substitute_templates(bindings))
774 .collect();
775 result.add_type(Atomic::TNamedObject {
776 fqcn: *fqcn,
777 type_params: vec_to_type_params(new_params),
778 });
779 }
780 Atomic::TClassString(Some(param_name)) => {
782 if let Some(resolved) = bindings.get(param_name) {
783 for r_atomic in &resolved.types {
784 let cls_name = if let Atomic::TNamedObject { fqcn, .. } = r_atomic {
785 Some(*fqcn)
786 } else {
787 None
788 };
789 result.add_type(Atomic::TClassString(cls_name));
790 }
791 } else {
792 result.add_type(atomic.clone());
793 }
794 }
795 _ => {
796 result.add_type(atomic.clone());
797 }
798 }
799 }
800 result
801 }
802
803 pub fn resolve_conditional_returns<F>(self, lookup: F) -> Type
809 where
810 F: Fn(&str) -> Option<Type>,
811 {
812 self.resolve_conditional_inner(&lookup)
813 }
814
815 fn resolve_conditional_inner<F>(self, lookup: &F) -> Type
816 where
817 F: Fn(&str) -> Option<Type>,
818 {
819 let mut result = Type::empty();
820 for atomic in self.types {
821 match atomic {
822 Atomic::TConditional {
823 ref param_name,
824 ref subject,
825 ref if_true,
826 ref if_false,
827 } => {
828 let resolved = if subject.types.len() == 1 {
829 if let Some(name) = param_name {
830 if let Some(arg_ty) = lookup(name.as_ref()) {
831 resolve_conditional_branch(
832 &subject.types[0],
833 &arg_ty,
834 if_true,
835 if_false,
836 )
837 } else {
838 None
839 }
840 } else {
841 None
842 }
843 } else {
844 None
845 };
846
847 if let Some(branch) = resolved {
848 for t in branch.resolve_conditional_inner(lookup).types {
850 result.add_type(t);
851 }
852 } else {
853 for t in if_true.clone().resolve_conditional_inner(lookup).types {
856 result.add_type(t);
857 }
858 for t in if_false.clone().resolve_conditional_inner(lookup).types {
859 result.add_type(t);
860 }
861 }
862 }
863 other => result.add_type(other),
864 }
865 }
866 result
867 }
868
869 pub fn is_subtype_structural(&self, other: &Type) -> bool {
879 if other.is_mixed() {
880 return true;
881 }
882 if self.is_never() {
883 return true; }
885 self.types
886 .iter()
887 .all(|a| other.types.iter().any(|b| atomic_subtype(a, b)))
888 }
889
890 fn filter<F: Fn(&Atomic) -> bool>(&self, f: F) -> Type {
893 let mut result = Type::empty();
894 result.possibly_undefined = self.possibly_undefined;
895 result.from_docblock = self.from_docblock;
896 for atomic in &self.types {
897 if f(atomic) {
898 result.types.push(atomic.clone());
899 }
900 }
901 result
902 }
903
904 pub fn possibly_undefined(mut self) -> Self {
906 self.possibly_undefined = true;
907 self
908 }
909
910 pub fn from_docblock(mut self) -> Self {
912 self.from_docblock = true;
913 self
914 }
915}
916
917fn is_string_atomic(a: &Atomic) -> bool {
922 matches!(
923 a,
924 Atomic::TString
925 | Atomic::TNonEmptyString
926 | Atomic::TLiteralString(_)
927 | Atomic::TNumericString
928 | Atomic::TClassString(_)
929 | Atomic::TCallableString
930 )
931}
932
933fn is_array_atomic(a: &Atomic) -> bool {
934 matches!(
935 a,
936 Atomic::TArray { .. }
937 | Atomic::TNonEmptyArray { .. }
938 | Atomic::TKeyedArray { .. }
939 | Atomic::TList { .. }
940 | Atomic::TNonEmptyList { .. }
941 )
942}
943
944fn is_list_atomic(a: &Atomic) -> bool {
945 match a {
946 Atomic::TList { .. } | Atomic::TNonEmptyList { .. } => true,
947 Atomic::TKeyedArray { is_list, .. } => *is_list,
948 _ => false,
949 }
950}
951
952fn resolve_conditional_branch(
958 subject: &Atomic,
959 arg_ty: &Type,
960 if_true: &Type,
961 if_false: &Type,
962) -> Option<Type> {
963 let predicate: fn(&Atomic) -> bool = match subject {
964 Atomic::TNull => |a| matches!(a, Atomic::TNull),
965 Atomic::TTrue => |a| matches!(a, Atomic::TTrue),
966 Atomic::TFalse => |a| matches!(a, Atomic::TFalse),
967 Atomic::TString => is_string_atomic,
968 Atomic::TList { .. } => is_list_atomic,
969 Atomic::TArray { .. } => is_array_atomic,
970 _ => return None,
971 };
972
973 if arg_ty.types.is_empty() {
974 return None;
975 }
976 let all_match = arg_ty.types.iter().all(&predicate);
977 let none_match = !arg_ty.types.iter().any(predicate);
978 if all_match {
979 Some(if_true.clone())
980 } else if none_match {
981 Some(if_false.clone())
982 } else {
983 None
984 }
985}
986
987fn substitute_in_fn_param(
992 p: &crate::atomic::FnParam,
993 bindings: &FxHashMap<Name, Type>,
994) -> crate::atomic::FnParam {
995 crate::atomic::FnParam {
996 name: p.name,
997 ty: p.ty.as_ref().map(|t| {
998 let u = t.to_union();
999 let substituted = u.substitute_templates(bindings);
1000 crate::compact::SimpleType::from_union(substituted)
1001 }),
1002 default: p.default.as_ref().map(|d| {
1003 let u = d.to_union();
1004 let substituted = u.substitute_templates(bindings);
1005 crate::compact::SimpleType::from_union(substituted)
1006 }),
1007 is_variadic: p.is_variadic,
1008 is_byref: p.is_byref,
1009 is_optional: p.is_optional,
1010 }
1011}
1012
1013fn atomic_subtype(sub: &Atomic, sup: &Atomic) -> bool {
1018 if sub == sup {
1019 return true;
1020 }
1021 match (sub, sup) {
1022 (Atomic::TNever, _) => true,
1024 (_, Atomic::TMixed) => true,
1026 (Atomic::TMixed, _) => true,
1027 (_, Atomic::TTemplateParam { as_type, .. }) => {
1032 as_type.is_mixed() || as_type.types.iter().any(|b| atomic_subtype(sub, b))
1033 }
1034
1035 (Atomic::TLiteralInt(_), Atomic::TInt) => true,
1037 (Atomic::TLiteralInt(_), Atomic::TNumeric) => true,
1038 (Atomic::TLiteralInt(_), Atomic::TScalar) => true,
1039 (Atomic::TLiteralInt(n), Atomic::TPositiveInt) => *n > 0,
1040 (Atomic::TLiteralInt(n), Atomic::TNonNegativeInt) => *n >= 0,
1041 (Atomic::TLiteralInt(n), Atomic::TNegativeInt) => *n < 0,
1042 (Atomic::TPositiveInt, Atomic::TInt) => true,
1043 (Atomic::TPositiveInt, Atomic::TNonNegativeInt) => true,
1044 (Atomic::TNegativeInt, Atomic::TInt) => true,
1045 (Atomic::TNonNegativeInt, Atomic::TInt) => true,
1046 (Atomic::TIntRange { .. }, Atomic::TInt) => true,
1047
1048 (Atomic::TLiteralFloat(..), Atomic::TFloat) => true,
1049 (Atomic::TLiteralFloat(..), Atomic::TNumeric) => true,
1050 (Atomic::TLiteralFloat(..), Atomic::TScalar) => true,
1051
1052 (Atomic::TLiteralString(s), Atomic::TString) => {
1053 let _ = s;
1054 true
1055 }
1056 (Atomic::TLiteralString(s), Atomic::TCallableString) => {
1057 let _ = s;
1058 true
1059 }
1060 (Atomic::TLiteralString(s), Atomic::TNonEmptyString) => !s.is_empty(),
1061 (Atomic::TLiteralString(s), Atomic::TNumericString) => s.parse::<f64>().is_ok(),
1062 (Atomic::TLiteralString(_), Atomic::TScalar) => true,
1063 (Atomic::TNonEmptyString, Atomic::TString) => true,
1064 (Atomic::TCallableString, Atomic::TString) => true,
1065 (Atomic::TNumericString, Atomic::TString) => true,
1066 (Atomic::TClassString(_), Atomic::TString) => true,
1067 (Atomic::TInterfaceString, Atomic::TString) => true,
1068 (Atomic::TEnumString, Atomic::TString) => true,
1069 (Atomic::TTraitString, Atomic::TString) => true,
1070
1071 (Atomic::TTrue, Atomic::TBool) => true,
1072 (Atomic::TFalse, Atomic::TBool) => true,
1073
1074 (Atomic::TInt, Atomic::TNumeric) => true,
1075 (Atomic::TFloat, Atomic::TNumeric) => true,
1076 (Atomic::TNumericString, Atomic::TNumeric) => true,
1077
1078 (Atomic::TInt, Atomic::TScalar) => true,
1079 (Atomic::TFloat, Atomic::TScalar) => true,
1080 (Atomic::TString, Atomic::TScalar) => true,
1081 (Atomic::TBool, Atomic::TScalar) => true,
1082 (Atomic::TNumeric, Atomic::TScalar) => true,
1083 (Atomic::TTrue, Atomic::TScalar) => true,
1084 (Atomic::TFalse, Atomic::TScalar) => true,
1085
1086 (Atomic::TNamedObject { .. }, Atomic::TObject) => true,
1088 (Atomic::TStaticObject { .. }, Atomic::TObject) => true,
1089 (Atomic::TSelf { .. }, Atomic::TObject) => true,
1090 (Atomic::TSelf { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1092 (Atomic::TStaticObject { fqcn: a }, Atomic::TNamedObject { fqcn: b, .. }) => a == b,
1093 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TSelf { fqcn: b }) => a == b,
1095 (Atomic::TNamedObject { fqcn: a, .. }, Atomic::TStaticObject { fqcn: b }) => a == b,
1096 (
1100 Atomic::TNamedObject {
1101 fqcn: sub_fqcn,
1102 type_params: sub_params,
1103 },
1104 Atomic::TNamedObject {
1105 fqcn: sup_fqcn,
1106 type_params: sup_params,
1107 },
1108 ) => sub_fqcn == sup_fqcn && (sup_params.is_empty() || sub_params == sup_params),
1109
1110 (Atomic::TLiteralInt(_), Atomic::TFloat) => true,
1112 (Atomic::TPositiveInt, Atomic::TFloat) => true,
1113 (Atomic::TInt, Atomic::TFloat) => true,
1114
1115 (Atomic::TLiteralInt(_), Atomic::TIntRange { .. }) => true,
1117
1118 (Atomic::TString, Atomic::TCallable { .. }) => true,
1120 (Atomic::TNonEmptyString, Atomic::TCallable { .. }) => true,
1121 (Atomic::TLiteralString(_), Atomic::TCallable { .. }) => true,
1122 (Atomic::TArray { .. }, Atomic::TCallable { .. }) => true,
1123 (Atomic::TNonEmptyArray { .. }, Atomic::TCallable { .. }) => true,
1124
1125 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1127 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1129 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1131 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1133 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1135 fqcn.as_ref().eq_ignore_ascii_case("closure")
1136 }
1137 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1138 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1140 fqcn.as_ref().eq_ignore_ascii_case("closure")
1141 }
1142
1143 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1145 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1147 && value.is_subtype_structural(av)
1148 }
1149 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1150 value.is_subtype_structural(lv)
1151 }
1152 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1154 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1155 && av.is_subtype_structural(lv)
1156 }
1157 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1158 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1159 && av.is_subtype_structural(lv)
1160 }
1161 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1162 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1163 && av.is_subtype_structural(lv)
1164 }
1165 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1166 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1167 && av.is_subtype_structural(lv)
1168 }
1169 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1171 (Atomic::TNonEmptyArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1172 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1173 }
1174
1175 (Atomic::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1177 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1178 }
1179
1180 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
1182
1183 (
1185 Atomic::TKeyedArray {
1186 properties,
1187 is_list,
1188 ..
1189 },
1190 Atomic::TList { value: lv },
1191 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1192 (
1193 Atomic::TKeyedArray {
1194 properties,
1195 is_list,
1196 ..
1197 },
1198 Atomic::TNonEmptyList { value: lv },
1199 ) => {
1200 *is_list
1201 && !properties.is_empty()
1202 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1203 }
1204
1205 _ => false,
1206 }
1207}
1208
1209#[cfg(test)]
1214mod tests {
1215 use std::sync::Arc;
1216
1217 use super::*;
1218
1219 #[test]
1220 fn single_is_single() {
1221 let u = Type::single(Atomic::TString);
1222 assert!(u.is_single());
1223 assert!(!u.is_nullable());
1224 }
1225
1226 #[test]
1227 fn nullable_has_null() {
1228 let u = Type::nullable(Atomic::TString);
1229 assert!(u.is_nullable());
1230 assert_eq!(u.types.len(), 2);
1231 }
1232
1233 #[test]
1234 fn add_type_deduplicates() {
1235 let mut u = Type::single(Atomic::TString);
1236 u.add_type(Atomic::TString);
1237 assert_eq!(u.types.len(), 1);
1238 }
1239
1240 #[test]
1241 fn add_type_literal_subsumed_by_base() {
1242 let mut u = Type::single(Atomic::TInt);
1243 u.add_type(Atomic::TLiteralInt(42));
1244 assert_eq!(u.types.len(), 1);
1245 assert!(matches!(u.types[0], Atomic::TInt));
1246 }
1247
1248 #[test]
1249 fn add_type_base_widens_literals() {
1250 let mut u = Type::single(Atomic::TLiteralInt(1));
1251 u.add_type(Atomic::TLiteralInt(2));
1252 u.add_type(Atomic::TInt);
1253 assert_eq!(u.types.len(), 1);
1254 assert!(matches!(u.types[0], Atomic::TInt));
1255 }
1256
1257 #[test]
1258 fn mixed_subsumes_everything() {
1259 let mut u = Type::single(Atomic::TString);
1260 u.add_type(Atomic::TMixed);
1261 assert_eq!(u.types.len(), 1);
1262 assert!(u.is_mixed());
1263 }
1264
1265 #[test]
1266 fn remove_null() {
1267 let u = Type::nullable(Atomic::TString);
1268 let narrowed = u.remove_null();
1269 assert!(!narrowed.is_nullable());
1270 assert_eq!(narrowed.types.len(), 1);
1271 }
1272
1273 #[test]
1274 fn narrow_to_truthy_removes_null_false() {
1275 let mut u = Type::empty();
1276 u.add_type(Atomic::TString);
1277 u.add_type(Atomic::TNull);
1278 u.add_type(Atomic::TFalse);
1279 let truthy = u.narrow_to_truthy();
1280 assert!(!truthy.is_nullable());
1281 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1282 }
1283
1284 #[test]
1285 fn merge_combines_types() {
1286 let a = Type::single(Atomic::TString);
1287 let b = Type::single(Atomic::TInt);
1288 let merged = Type::merge(&a, &b);
1289 assert_eq!(merged.types.len(), 2);
1290 }
1291
1292 #[test]
1293 fn subtype_literal_int_under_int() {
1294 let sub = Type::single(Atomic::TLiteralInt(5));
1295 let sup = Type::single(Atomic::TInt);
1296 assert!(sub.is_subtype_structural(&sup));
1297 }
1298
1299 #[test]
1300 fn subtype_never_is_bottom() {
1301 let never = Type::never();
1302 let string = Type::single(Atomic::TString);
1303 assert!(never.is_subtype_structural(&string));
1304 }
1305
1306 #[test]
1307 fn subtype_everything_under_mixed() {
1308 let string = Type::single(Atomic::TString);
1309 let mixed = Type::mixed();
1310 assert!(string.is_subtype_structural(&mixed));
1311 }
1312
1313 #[test]
1314 fn template_substitution() {
1315 let mut bindings = FxHashMap::default();
1316 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1317
1318 let tmpl = Type::single(Atomic::TTemplateParam {
1319 name: Name::new("T"),
1320 as_type: Box::new(Type::mixed()),
1321 defining_entity: Name::new("MyClass"),
1322 });
1323
1324 let resolved = tmpl.substitute_templates(&bindings);
1325 assert_eq!(resolved.types.len(), 1);
1326 assert!(matches!(resolved.types[0], Atomic::TString));
1327 }
1328
1329 #[test]
1330 fn intersection_is_object() {
1331 let parts = vec![
1332 Type::single(Atomic::TNamedObject {
1333 fqcn: Name::new("Iterator"),
1334 type_params: empty_type_params(),
1335 }),
1336 Type::single(Atomic::TNamedObject {
1337 fqcn: Name::new("Countable"),
1338 type_params: empty_type_params(),
1339 }),
1340 ];
1341 let atomic = Atomic::TIntersection {
1342 parts: vec_to_type_params(parts),
1343 };
1344 assert!(atomic.is_object());
1345 assert!(!atomic.can_be_falsy());
1346 assert!(atomic.can_be_truthy());
1347 }
1348
1349 #[test]
1350 fn intersection_display_two_parts() {
1351 let parts = vec![
1352 Type::single(Atomic::TNamedObject {
1353 fqcn: Name::new("Iterator"),
1354 type_params: empty_type_params(),
1355 }),
1356 Type::single(Atomic::TNamedObject {
1357 fqcn: Name::new("Countable"),
1358 type_params: empty_type_params(),
1359 }),
1360 ];
1361 let u = Type::single(Atomic::TIntersection {
1362 parts: vec_to_type_params(parts),
1363 });
1364 assert_eq!(format!("{u}"), "Iterator&Countable");
1365 }
1366
1367 #[test]
1368 fn intersection_display_three_parts() {
1369 let parts = vec![
1370 Type::single(Atomic::TNamedObject {
1371 fqcn: Name::new("A"),
1372 type_params: empty_type_params(),
1373 }),
1374 Type::single(Atomic::TNamedObject {
1375 fqcn: Name::new("B"),
1376 type_params: empty_type_params(),
1377 }),
1378 Type::single(Atomic::TNamedObject {
1379 fqcn: Name::new("C"),
1380 type_params: empty_type_params(),
1381 }),
1382 ];
1383 let u = Type::single(Atomic::TIntersection {
1384 parts: vec_to_type_params(parts),
1385 });
1386 assert_eq!(format!("{u}"), "A&B&C");
1387 }
1388
1389 #[test]
1390 fn intersection_in_nullable_union_display() {
1391 let intersection = Atomic::TIntersection {
1392 parts: vec_to_type_params(vec![
1393 Type::single(Atomic::TNamedObject {
1394 fqcn: Name::new("Iterator"),
1395 type_params: empty_type_params(),
1396 }),
1397 Type::single(Atomic::TNamedObject {
1398 fqcn: Name::new("Countable"),
1399 type_params: empty_type_params(),
1400 }),
1401 ]),
1402 };
1403 let mut u = Type::single(intersection);
1404 u.add_type(Atomic::TNull);
1405 assert!(u.is_nullable());
1406 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1407 }
1408
1409 fn t_param(name: &str) -> Type {
1412 Type::single(Atomic::TTemplateParam {
1413 name: Name::new(name),
1414 as_type: Box::new(Type::mixed()),
1415 defining_entity: Name::new("Fn"),
1416 })
1417 }
1418
1419 fn bindings_t_string() -> FxHashMap<Name, Type> {
1420 let mut b = FxHashMap::default();
1421 b.insert(Name::new("T"), Type::single(Atomic::TString));
1422 b
1423 }
1424
1425 #[test]
1426 fn substitute_non_empty_array_key_and_value() {
1427 let ty = Type::single(Atomic::TNonEmptyArray {
1428 key: Box::new(t_param("T")),
1429 value: Box::new(t_param("T")),
1430 });
1431 let result = ty.substitute_templates(&bindings_t_string());
1432 assert_eq!(result.types.len(), 1);
1433 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1434 panic!("expected TNonEmptyArray");
1435 };
1436 assert!(matches!(key.types[0], Atomic::TString));
1437 assert!(matches!(value.types[0], Atomic::TString));
1438 }
1439
1440 #[test]
1441 fn substitute_non_empty_list_value() {
1442 let ty = Type::single(Atomic::TNonEmptyList {
1443 value: Box::new(t_param("T")),
1444 });
1445 let result = ty.substitute_templates(&bindings_t_string());
1446 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1447 panic!("expected TNonEmptyList");
1448 };
1449 assert!(matches!(value.types[0], Atomic::TString));
1450 }
1451
1452 #[test]
1453 fn substitute_keyed_array_property_types() {
1454 use crate::atomic::{ArrayKey, KeyedProperty};
1455 use indexmap::IndexMap;
1456 let mut props = IndexMap::new();
1457 props.insert(
1458 ArrayKey::String(Arc::from("name")),
1459 KeyedProperty {
1460 ty: t_param("T"),
1461 optional: false,
1462 },
1463 );
1464 props.insert(
1465 ArrayKey::String(Arc::from("tag")),
1466 KeyedProperty {
1467 ty: t_param("T"),
1468 optional: true,
1469 },
1470 );
1471 let ty = Type::single(Atomic::TKeyedArray {
1472 properties: props,
1473 is_open: true,
1474 is_list: false,
1475 });
1476 let result = ty.substitute_templates(&bindings_t_string());
1477 let Atomic::TKeyedArray {
1478 properties,
1479 is_open,
1480 is_list,
1481 } = &result.types[0]
1482 else {
1483 panic!("expected TKeyedArray");
1484 };
1485 assert!(is_open);
1486 assert!(!is_list);
1487 assert!(matches!(
1488 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1489 Atomic::TString
1490 ));
1491 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1492 assert!(matches!(
1493 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1494 Atomic::TString
1495 ));
1496 }
1497
1498 #[test]
1499 fn substitute_callable_params_and_return() {
1500 use crate::atomic::FnParam;
1501 let ty = Type::single(Atomic::TCallable {
1502 params: Some(vec![FnParam {
1503 name: Name::new("x"),
1504 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1505 default: None,
1506 is_variadic: false,
1507 is_byref: false,
1508 is_optional: false,
1509 }]),
1510 return_type: Some(Box::new(t_param("T"))),
1511 });
1512 let result = ty.substitute_templates(&bindings_t_string());
1513 let Atomic::TCallable {
1514 params,
1515 return_type,
1516 } = &result.types[0]
1517 else {
1518 panic!("expected TCallable");
1519 };
1520 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1521 let param_union = param_ty.to_union();
1522 assert!(matches!(param_union.types[0], Atomic::TString));
1523 let ret = return_type.as_ref().unwrap();
1524 assert!(matches!(ret.types[0], Atomic::TString));
1525 }
1526
1527 #[test]
1528 fn substitute_callable_bare_no_panic() {
1529 let ty = Type::single(Atomic::TCallable {
1531 params: None,
1532 return_type: None,
1533 });
1534 let result = ty.substitute_templates(&bindings_t_string());
1535 assert!(matches!(
1536 result.types[0],
1537 Atomic::TCallable {
1538 params: None,
1539 return_type: None
1540 }
1541 ));
1542 }
1543
1544 #[test]
1545 fn substitute_closure_params_return_and_this() {
1546 use crate::atomic::FnParam;
1547 let ty = Type::single(Atomic::TClosure {
1548 params: vec![FnParam {
1549 name: Name::new("a"),
1550 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1551 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1552 is_variadic: true,
1553 is_byref: true,
1554 is_optional: true,
1555 }],
1556 return_type: Box::new(t_param("T")),
1557 this_type: Some(Box::new(t_param("T"))),
1558 });
1559 let result = ty.substitute_templates(&bindings_t_string());
1560 let Atomic::TClosure {
1561 params,
1562 return_type,
1563 this_type,
1564 } = &result.types[0]
1565 else {
1566 panic!("expected TClosure");
1567 };
1568 let p = ¶ms[0];
1569 let ty_union = p.ty.as_ref().unwrap().to_union();
1570 let default_union = p.default.as_ref().unwrap().to_union();
1571 assert!(matches!(ty_union.types[0], Atomic::TString));
1572 assert!(matches!(default_union.types[0], Atomic::TString));
1573 assert!(p.is_variadic);
1575 assert!(p.is_byref);
1576 assert!(p.is_optional);
1577 assert!(matches!(return_type.types[0], Atomic::TString));
1578 assert!(matches!(
1579 this_type.as_ref().unwrap().types[0],
1580 Atomic::TString
1581 ));
1582 }
1583
1584 #[test]
1585 fn substitute_conditional_all_branches() {
1586 let ty = Type::single(Atomic::TConditional {
1587 param_name: None,
1588 subject: Box::new(t_param("T")),
1589 if_true: Box::new(t_param("T")),
1590 if_false: Box::new(Type::single(Atomic::TInt)),
1591 });
1592 let result = ty.substitute_templates(&bindings_t_string());
1593 let Atomic::TConditional {
1594 param_name: _,
1595 subject,
1596 if_true,
1597 if_false,
1598 } = &result.types[0]
1599 else {
1600 panic!("expected TConditional");
1601 };
1602 assert!(matches!(subject.types[0], Atomic::TString));
1603 assert!(matches!(if_true.types[0], Atomic::TString));
1604 assert!(matches!(if_false.types[0], Atomic::TInt));
1605 }
1606
1607 #[test]
1608 fn resolve_conditional_is_null_non_null_arg() {
1609 let ty = Type::single(Atomic::TConditional {
1610 param_name: Some(Name::new("x")),
1611 subject: Box::new(Type::single(Atomic::TNull)),
1612 if_true: Box::new(Type::single(Atomic::TInt)),
1613 if_false: Box::new(Type::single(Atomic::TString)),
1614 });
1615 let result = ty.resolve_conditional_returns(|name| {
1616 if name == "x" {
1617 Some(Type::single(Atomic::TString)) } else {
1619 None
1620 }
1621 });
1622 assert!(result.types.len() == 1);
1623 assert!(matches!(result.types[0], Atomic::TString));
1624 }
1625
1626 #[test]
1627 fn resolve_conditional_is_null_null_arg() {
1628 let ty = Type::single(Atomic::TConditional {
1629 param_name: Some(Name::new("x")),
1630 subject: Box::new(Type::single(Atomic::TNull)),
1631 if_true: Box::new(Type::single(Atomic::TInt)),
1632 if_false: Box::new(Type::single(Atomic::TString)),
1633 });
1634 let result = ty.resolve_conditional_returns(|name| {
1635 if name == "x" {
1636 Some(Type::single(Atomic::TNull)) } else {
1638 None
1639 }
1640 });
1641 assert!(result.types.len() == 1);
1642 assert!(matches!(result.types[0], Atomic::TInt));
1643 }
1644
1645 #[test]
1646 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1647 let mut nullable_str = Type::single(Atomic::TString);
1648 nullable_str.add_type(Atomic::TNull);
1649 let ty = Type::single(Atomic::TConditional {
1650 param_name: Some(Name::new("x")),
1651 subject: Box::new(Type::single(Atomic::TNull)),
1652 if_true: Box::new(Type::single(Atomic::TInt)),
1653 if_false: Box::new(Type::single(Atomic::TString)),
1654 });
1655 let result = ty.resolve_conditional_returns(|name| {
1656 if name == "x" {
1657 Some(nullable_str.clone())
1658 } else {
1659 None
1660 }
1661 });
1662 assert_eq!(result.types.len(), 2);
1664 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1665 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1666 }
1667
1668 #[test]
1669 fn resolve_conditional_nested_widens_inner_branch() {
1670 let inner = Type::single(Atomic::TConditional {
1673 param_name: Some(Name::new("x")),
1674 subject: Box::new(Type::single(Atomic::TString)),
1675 if_true: Box::new(Type::single(Atomic::TString)),
1676 if_false: Box::new(Type::single(Atomic::TFloat)),
1677 });
1678 let ty = Type::single(Atomic::TConditional {
1679 param_name: Some(Name::new("x")),
1680 subject: Box::new(Type::single(Atomic::TNull)),
1681 if_true: Box::new(Type::single(Atomic::TInt)),
1682 if_false: Box::new(inner),
1683 });
1684 let result = ty.resolve_conditional_returns(|_| None);
1686 assert!(
1687 result
1688 .types
1689 .iter()
1690 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1691 "no TConditional should survive: {:?}",
1692 result.types
1693 );
1694 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1695 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1696 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
1697 }
1698
1699 #[test]
1700 fn resolve_conditional_nested_resolves_inner_branch() {
1701 let inner = Type::single(Atomic::TConditional {
1705 param_name: Some(Name::new("x")),
1706 subject: Box::new(Type::single(Atomic::TString)),
1707 if_true: Box::new(Type::single(Atomic::TString)),
1708 if_false: Box::new(Type::single(Atomic::TFloat)),
1709 });
1710 let ty = Type::single(Atomic::TConditional {
1711 param_name: Some(Name::new("x")),
1712 subject: Box::new(Type::single(Atomic::TNull)),
1713 if_true: Box::new(Type::single(Atomic::TInt)),
1714 if_false: Box::new(inner),
1715 });
1716 let result = ty.resolve_conditional_returns(|name| {
1718 if name == "x" {
1719 Some(Type::single(Atomic::TString))
1720 } else {
1721 None
1722 }
1723 });
1724 assert!(
1725 result
1726 .types
1727 .iter()
1728 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1729 "no TConditional should survive: {:?}",
1730 result.types
1731 );
1732 assert_eq!(result.types.len(), 1);
1733 assert!(matches!(result.types[0], Atomic::TString));
1734 }
1735
1736 #[test]
1737 fn substitute_intersection_parts() {
1738 let ty = Type::single(Atomic::TIntersection {
1739 parts: vec_to_type_params(vec![
1740 Type::single(Atomic::TNamedObject {
1741 fqcn: Name::new("Countable"),
1742 type_params: empty_type_params(),
1743 }),
1744 t_param("T"),
1745 ]),
1746 });
1747 let result = ty.substitute_templates(&bindings_t_string());
1748 let Atomic::TIntersection { parts } = &result.types[0] else {
1749 panic!("expected TIntersection");
1750 };
1751 assert_eq!(parts.len(), 2);
1752 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1753 assert!(matches!(parts[1].types[0], Atomic::TString));
1754 }
1755
1756 #[test]
1757 fn substitute_no_template_params_identity() {
1758 let ty = Type::single(Atomic::TInt);
1759 let result = ty.substitute_templates(&bindings_t_string());
1760 assert!(matches!(result.types[0], Atomic::TInt));
1761 }
1762}