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 (Atomic::TKeyedArray { .. }, Atomic::TCallable { .. }) => true,
1125
1126 (Atomic::TClosure { .. }, Atomic::TCallable { .. }) => true,
1128 (Atomic::TCallable { .. }, Atomic::TClosure { .. }) => true,
1130 (Atomic::TClosure { .. }, Atomic::TClosure { .. }) => true,
1132 (Atomic::TCallable { .. }, Atomic::TCallable { .. }) => true,
1134 (Atomic::TClosure { .. }, Atomic::TNamedObject { fqcn, .. }) => {
1136 fqcn.as_ref().eq_ignore_ascii_case("closure")
1137 }
1138 (Atomic::TClosure { .. }, Atomic::TObject) => true,
1139 (Atomic::TNamedObject { fqcn, .. }, Atomic::TClosure { .. }) => {
1141 fqcn.as_ref().eq_ignore_ascii_case("closure")
1142 }
1143 (Atomic::TNamedObject { fqcn, .. }, Atomic::TCallable { .. }) => {
1145 fqcn.as_ref().eq_ignore_ascii_case("closure")
1146 }
1147
1148 (Atomic::TList { value }, Atomic::TArray { key, value: av }) => {
1150 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1152 && value.is_subtype_structural(av)
1153 }
1154 (Atomic::TNonEmptyList { value }, Atomic::TList { value: lv }) => {
1155 value.is_subtype_structural(lv)
1156 }
1157 (Atomic::TArray { key, value: av }, Atomic::TList { value: lv }) => {
1159 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1160 && av.is_subtype_structural(lv)
1161 }
1162 (Atomic::TArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1163 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1164 && av.is_subtype_structural(lv)
1165 }
1166 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TList { value: lv }) => {
1167 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1168 && av.is_subtype_structural(lv)
1169 }
1170 (Atomic::TNonEmptyArray { key, value: av }, Atomic::TNonEmptyList { value: lv }) => {
1171 matches!(key.types.as_slice(), [Atomic::TInt | Atomic::TMixed])
1172 && av.is_subtype_structural(lv)
1173 }
1174 (Atomic::TList { value: v1 }, Atomic::TList { value: v2 }) => v1.is_subtype_structural(v2),
1176 (Atomic::TNonEmptyArray { 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::TArray { key: k1, value: v1 }, Atomic::TArray { key: k2, value: v2 }) => {
1182 k1.is_subtype_structural(k2) && v1.is_subtype_structural(v2)
1183 }
1184
1185 (Atomic::TKeyedArray { .. }, Atomic::TArray { .. }) => true,
1187
1188 (
1190 Atomic::TKeyedArray {
1191 properties,
1192 is_list,
1193 ..
1194 },
1195 Atomic::TList { value: lv },
1196 ) => *is_list && properties.values().all(|p| p.ty.is_subtype_structural(lv)),
1197 (
1198 Atomic::TKeyedArray {
1199 properties,
1200 is_list,
1201 ..
1202 },
1203 Atomic::TNonEmptyList { value: lv },
1204 ) => {
1205 *is_list
1206 && !properties.is_empty()
1207 && properties.values().all(|p| p.ty.is_subtype_structural(lv))
1208 }
1209
1210 _ => false,
1211 }
1212}
1213
1214#[cfg(test)]
1219mod tests {
1220 use std::sync::Arc;
1221
1222 use super::*;
1223
1224 #[test]
1225 fn single_is_single() {
1226 let u = Type::single(Atomic::TString);
1227 assert!(u.is_single());
1228 assert!(!u.is_nullable());
1229 }
1230
1231 #[test]
1232 fn nullable_has_null() {
1233 let u = Type::nullable(Atomic::TString);
1234 assert!(u.is_nullable());
1235 assert_eq!(u.types.len(), 2);
1236 }
1237
1238 #[test]
1239 fn add_type_deduplicates() {
1240 let mut u = Type::single(Atomic::TString);
1241 u.add_type(Atomic::TString);
1242 assert_eq!(u.types.len(), 1);
1243 }
1244
1245 #[test]
1246 fn add_type_literal_subsumed_by_base() {
1247 let mut u = Type::single(Atomic::TInt);
1248 u.add_type(Atomic::TLiteralInt(42));
1249 assert_eq!(u.types.len(), 1);
1250 assert!(matches!(u.types[0], Atomic::TInt));
1251 }
1252
1253 #[test]
1254 fn add_type_base_widens_literals() {
1255 let mut u = Type::single(Atomic::TLiteralInt(1));
1256 u.add_type(Atomic::TLiteralInt(2));
1257 u.add_type(Atomic::TInt);
1258 assert_eq!(u.types.len(), 1);
1259 assert!(matches!(u.types[0], Atomic::TInt));
1260 }
1261
1262 #[test]
1263 fn mixed_subsumes_everything() {
1264 let mut u = Type::single(Atomic::TString);
1265 u.add_type(Atomic::TMixed);
1266 assert_eq!(u.types.len(), 1);
1267 assert!(u.is_mixed());
1268 }
1269
1270 #[test]
1271 fn remove_null() {
1272 let u = Type::nullable(Atomic::TString);
1273 let narrowed = u.remove_null();
1274 assert!(!narrowed.is_nullable());
1275 assert_eq!(narrowed.types.len(), 1);
1276 }
1277
1278 #[test]
1279 fn narrow_to_truthy_removes_null_false() {
1280 let mut u = Type::empty();
1281 u.add_type(Atomic::TString);
1282 u.add_type(Atomic::TNull);
1283 u.add_type(Atomic::TFalse);
1284 let truthy = u.narrow_to_truthy();
1285 assert!(!truthy.is_nullable());
1286 assert!(!truthy.contains(|t| matches!(t, Atomic::TFalse)));
1287 }
1288
1289 #[test]
1290 fn merge_combines_types() {
1291 let a = Type::single(Atomic::TString);
1292 let b = Type::single(Atomic::TInt);
1293 let merged = Type::merge(&a, &b);
1294 assert_eq!(merged.types.len(), 2);
1295 }
1296
1297 #[test]
1298 fn subtype_literal_int_under_int() {
1299 let sub = Type::single(Atomic::TLiteralInt(5));
1300 let sup = Type::single(Atomic::TInt);
1301 assert!(sub.is_subtype_structural(&sup));
1302 }
1303
1304 #[test]
1305 fn subtype_never_is_bottom() {
1306 let never = Type::never();
1307 let string = Type::single(Atomic::TString);
1308 assert!(never.is_subtype_structural(&string));
1309 }
1310
1311 #[test]
1312 fn subtype_everything_under_mixed() {
1313 let string = Type::single(Atomic::TString);
1314 let mixed = Type::mixed();
1315 assert!(string.is_subtype_structural(&mixed));
1316 }
1317
1318 #[test]
1319 fn template_substitution() {
1320 let mut bindings = FxHashMap::default();
1321 bindings.insert(Name::new("T"), Type::single(Atomic::TString));
1322
1323 let tmpl = Type::single(Atomic::TTemplateParam {
1324 name: Name::new("T"),
1325 as_type: Box::new(Type::mixed()),
1326 defining_entity: Name::new("MyClass"),
1327 });
1328
1329 let resolved = tmpl.substitute_templates(&bindings);
1330 assert_eq!(resolved.types.len(), 1);
1331 assert!(matches!(resolved.types[0], Atomic::TString));
1332 }
1333
1334 #[test]
1335 fn intersection_is_object() {
1336 let parts = vec![
1337 Type::single(Atomic::TNamedObject {
1338 fqcn: Name::new("Iterator"),
1339 type_params: empty_type_params(),
1340 }),
1341 Type::single(Atomic::TNamedObject {
1342 fqcn: Name::new("Countable"),
1343 type_params: empty_type_params(),
1344 }),
1345 ];
1346 let atomic = Atomic::TIntersection {
1347 parts: vec_to_type_params(parts),
1348 };
1349 assert!(atomic.is_object());
1350 assert!(!atomic.can_be_falsy());
1351 assert!(atomic.can_be_truthy());
1352 }
1353
1354 #[test]
1355 fn intersection_display_two_parts() {
1356 let parts = vec![
1357 Type::single(Atomic::TNamedObject {
1358 fqcn: Name::new("Iterator"),
1359 type_params: empty_type_params(),
1360 }),
1361 Type::single(Atomic::TNamedObject {
1362 fqcn: Name::new("Countable"),
1363 type_params: empty_type_params(),
1364 }),
1365 ];
1366 let u = Type::single(Atomic::TIntersection {
1367 parts: vec_to_type_params(parts),
1368 });
1369 assert_eq!(format!("{u}"), "Iterator&Countable");
1370 }
1371
1372 #[test]
1373 fn intersection_display_three_parts() {
1374 let parts = vec![
1375 Type::single(Atomic::TNamedObject {
1376 fqcn: Name::new("A"),
1377 type_params: empty_type_params(),
1378 }),
1379 Type::single(Atomic::TNamedObject {
1380 fqcn: Name::new("B"),
1381 type_params: empty_type_params(),
1382 }),
1383 Type::single(Atomic::TNamedObject {
1384 fqcn: Name::new("C"),
1385 type_params: empty_type_params(),
1386 }),
1387 ];
1388 let u = Type::single(Atomic::TIntersection {
1389 parts: vec_to_type_params(parts),
1390 });
1391 assert_eq!(format!("{u}"), "A&B&C");
1392 }
1393
1394 #[test]
1395 fn intersection_in_nullable_union_display() {
1396 let intersection = Atomic::TIntersection {
1397 parts: vec_to_type_params(vec![
1398 Type::single(Atomic::TNamedObject {
1399 fqcn: Name::new("Iterator"),
1400 type_params: empty_type_params(),
1401 }),
1402 Type::single(Atomic::TNamedObject {
1403 fqcn: Name::new("Countable"),
1404 type_params: empty_type_params(),
1405 }),
1406 ]),
1407 };
1408 let mut u = Type::single(intersection);
1409 u.add_type(Atomic::TNull);
1410 assert!(u.is_nullable());
1411 assert!(u.contains(|t| matches!(t, Atomic::TIntersection { .. })));
1412 }
1413
1414 fn t_param(name: &str) -> Type {
1417 Type::single(Atomic::TTemplateParam {
1418 name: Name::new(name),
1419 as_type: Box::new(Type::mixed()),
1420 defining_entity: Name::new("Fn"),
1421 })
1422 }
1423
1424 fn bindings_t_string() -> FxHashMap<Name, Type> {
1425 let mut b = FxHashMap::default();
1426 b.insert(Name::new("T"), Type::single(Atomic::TString));
1427 b
1428 }
1429
1430 #[test]
1431 fn substitute_non_empty_array_key_and_value() {
1432 let ty = Type::single(Atomic::TNonEmptyArray {
1433 key: Box::new(t_param("T")),
1434 value: Box::new(t_param("T")),
1435 });
1436 let result = ty.substitute_templates(&bindings_t_string());
1437 assert_eq!(result.types.len(), 1);
1438 let Atomic::TNonEmptyArray { key, value } = &result.types[0] else {
1439 panic!("expected TNonEmptyArray");
1440 };
1441 assert!(matches!(key.types[0], Atomic::TString));
1442 assert!(matches!(value.types[0], Atomic::TString));
1443 }
1444
1445 #[test]
1446 fn substitute_non_empty_list_value() {
1447 let ty = Type::single(Atomic::TNonEmptyList {
1448 value: Box::new(t_param("T")),
1449 });
1450 let result = ty.substitute_templates(&bindings_t_string());
1451 let Atomic::TNonEmptyList { value } = &result.types[0] else {
1452 panic!("expected TNonEmptyList");
1453 };
1454 assert!(matches!(value.types[0], Atomic::TString));
1455 }
1456
1457 #[test]
1458 fn substitute_keyed_array_property_types() {
1459 use crate::atomic::{ArrayKey, KeyedProperty};
1460 use indexmap::IndexMap;
1461 let mut props = IndexMap::new();
1462 props.insert(
1463 ArrayKey::String(Arc::from("name")),
1464 KeyedProperty {
1465 ty: t_param("T"),
1466 optional: false,
1467 },
1468 );
1469 props.insert(
1470 ArrayKey::String(Arc::from("tag")),
1471 KeyedProperty {
1472 ty: t_param("T"),
1473 optional: true,
1474 },
1475 );
1476 let ty = Type::single(Atomic::TKeyedArray {
1477 properties: props,
1478 is_open: true,
1479 is_list: false,
1480 });
1481 let result = ty.substitute_templates(&bindings_t_string());
1482 let Atomic::TKeyedArray {
1483 properties,
1484 is_open,
1485 is_list,
1486 } = &result.types[0]
1487 else {
1488 panic!("expected TKeyedArray");
1489 };
1490 assert!(is_open);
1491 assert!(!is_list);
1492 assert!(matches!(
1493 properties[&ArrayKey::String(Arc::from("name"))].ty.types[0],
1494 Atomic::TString
1495 ));
1496 assert!(properties[&ArrayKey::String(Arc::from("tag"))].optional);
1497 assert!(matches!(
1498 properties[&ArrayKey::String(Arc::from("tag"))].ty.types[0],
1499 Atomic::TString
1500 ));
1501 }
1502
1503 #[test]
1504 fn substitute_callable_params_and_return() {
1505 use crate::atomic::FnParam;
1506 let ty = Type::single(Atomic::TCallable {
1507 params: Some(vec![FnParam {
1508 name: Name::new("x"),
1509 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1510 default: None,
1511 is_variadic: false,
1512 is_byref: false,
1513 is_optional: false,
1514 }]),
1515 return_type: Some(Box::new(t_param("T"))),
1516 });
1517 let result = ty.substitute_templates(&bindings_t_string());
1518 let Atomic::TCallable {
1519 params,
1520 return_type,
1521 } = &result.types[0]
1522 else {
1523 panic!("expected TCallable");
1524 };
1525 let param_ty = params.as_ref().unwrap()[0].ty.as_ref().unwrap();
1526 let param_union = param_ty.to_union();
1527 assert!(matches!(param_union.types[0], Atomic::TString));
1528 let ret = return_type.as_ref().unwrap();
1529 assert!(matches!(ret.types[0], Atomic::TString));
1530 }
1531
1532 #[test]
1533 fn substitute_callable_bare_no_panic() {
1534 let ty = Type::single(Atomic::TCallable {
1536 params: None,
1537 return_type: None,
1538 });
1539 let result = ty.substitute_templates(&bindings_t_string());
1540 assert!(matches!(
1541 result.types[0],
1542 Atomic::TCallable {
1543 params: None,
1544 return_type: None
1545 }
1546 ));
1547 }
1548
1549 #[test]
1550 fn substitute_closure_params_return_and_this() {
1551 use crate::atomic::FnParam;
1552 let ty = Type::single(Atomic::TClosure {
1553 params: vec![FnParam {
1554 name: Name::new("a"),
1555 ty: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1556 default: Some(crate::compact::SimpleType::from_union(t_param("T"))),
1557 is_variadic: true,
1558 is_byref: true,
1559 is_optional: true,
1560 }],
1561 return_type: Box::new(t_param("T")),
1562 this_type: Some(Box::new(t_param("T"))),
1563 });
1564 let result = ty.substitute_templates(&bindings_t_string());
1565 let Atomic::TClosure {
1566 params,
1567 return_type,
1568 this_type,
1569 } = &result.types[0]
1570 else {
1571 panic!("expected TClosure");
1572 };
1573 let p = ¶ms[0];
1574 let ty_union = p.ty.as_ref().unwrap().to_union();
1575 let default_union = p.default.as_ref().unwrap().to_union();
1576 assert!(matches!(ty_union.types[0], Atomic::TString));
1577 assert!(matches!(default_union.types[0], Atomic::TString));
1578 assert!(p.is_variadic);
1580 assert!(p.is_byref);
1581 assert!(p.is_optional);
1582 assert!(matches!(return_type.types[0], Atomic::TString));
1583 assert!(matches!(
1584 this_type.as_ref().unwrap().types[0],
1585 Atomic::TString
1586 ));
1587 }
1588
1589 #[test]
1590 fn substitute_conditional_all_branches() {
1591 let ty = Type::single(Atomic::TConditional {
1592 param_name: None,
1593 subject: Box::new(t_param("T")),
1594 if_true: Box::new(t_param("T")),
1595 if_false: Box::new(Type::single(Atomic::TInt)),
1596 });
1597 let result = ty.substitute_templates(&bindings_t_string());
1598 let Atomic::TConditional {
1599 param_name: _,
1600 subject,
1601 if_true,
1602 if_false,
1603 } = &result.types[0]
1604 else {
1605 panic!("expected TConditional");
1606 };
1607 assert!(matches!(subject.types[0], Atomic::TString));
1608 assert!(matches!(if_true.types[0], Atomic::TString));
1609 assert!(matches!(if_false.types[0], Atomic::TInt));
1610 }
1611
1612 #[test]
1613 fn resolve_conditional_is_null_non_null_arg() {
1614 let ty = Type::single(Atomic::TConditional {
1615 param_name: Some(Name::new("x")),
1616 subject: Box::new(Type::single(Atomic::TNull)),
1617 if_true: Box::new(Type::single(Atomic::TInt)),
1618 if_false: Box::new(Type::single(Atomic::TString)),
1619 });
1620 let result = ty.resolve_conditional_returns(|name| {
1621 if name == "x" {
1622 Some(Type::single(Atomic::TString)) } else {
1624 None
1625 }
1626 });
1627 assert!(result.types.len() == 1);
1628 assert!(matches!(result.types[0], Atomic::TString));
1629 }
1630
1631 #[test]
1632 fn resolve_conditional_is_null_null_arg() {
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(Type::single(Atomic::TNull)) } else {
1643 None
1644 }
1645 });
1646 assert!(result.types.len() == 1);
1647 assert!(matches!(result.types[0], Atomic::TInt));
1648 }
1649
1650 #[test]
1651 fn resolve_conditional_is_null_nullable_arg_widens_to_branch_union() {
1652 let mut nullable_str = Type::single(Atomic::TString);
1653 nullable_str.add_type(Atomic::TNull);
1654 let ty = Type::single(Atomic::TConditional {
1655 param_name: Some(Name::new("x")),
1656 subject: Box::new(Type::single(Atomic::TNull)),
1657 if_true: Box::new(Type::single(Atomic::TInt)),
1658 if_false: Box::new(Type::single(Atomic::TString)),
1659 });
1660 let result = ty.resolve_conditional_returns(|name| {
1661 if name == "x" {
1662 Some(nullable_str.clone())
1663 } else {
1664 None
1665 }
1666 });
1667 assert_eq!(result.types.len(), 2);
1669 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1670 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1671 }
1672
1673 #[test]
1674 fn resolve_conditional_nested_widens_inner_branch() {
1675 let inner = Type::single(Atomic::TConditional {
1678 param_name: Some(Name::new("x")),
1679 subject: Box::new(Type::single(Atomic::TString)),
1680 if_true: Box::new(Type::single(Atomic::TString)),
1681 if_false: Box::new(Type::single(Atomic::TFloat)),
1682 });
1683 let ty = Type::single(Atomic::TConditional {
1684 param_name: Some(Name::new("x")),
1685 subject: Box::new(Type::single(Atomic::TNull)),
1686 if_true: Box::new(Type::single(Atomic::TInt)),
1687 if_false: Box::new(inner),
1688 });
1689 let result = ty.resolve_conditional_returns(|_| None);
1691 assert!(
1692 result
1693 .types
1694 .iter()
1695 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1696 "no TConditional should survive: {:?}",
1697 result.types
1698 );
1699 assert!(result.types.iter().any(|t| matches!(t, Atomic::TInt)));
1700 assert!(result.types.iter().any(|t| matches!(t, Atomic::TString)));
1701 assert!(result.types.iter().any(|t| matches!(t, Atomic::TFloat)));
1702 }
1703
1704 #[test]
1705 fn resolve_conditional_nested_resolves_inner_branch() {
1706 let inner = Type::single(Atomic::TConditional {
1710 param_name: Some(Name::new("x")),
1711 subject: Box::new(Type::single(Atomic::TString)),
1712 if_true: Box::new(Type::single(Atomic::TString)),
1713 if_false: Box::new(Type::single(Atomic::TFloat)),
1714 });
1715 let ty = Type::single(Atomic::TConditional {
1716 param_name: Some(Name::new("x")),
1717 subject: Box::new(Type::single(Atomic::TNull)),
1718 if_true: Box::new(Type::single(Atomic::TInt)),
1719 if_false: Box::new(inner),
1720 });
1721 let result = ty.resolve_conditional_returns(|name| {
1723 if name == "x" {
1724 Some(Type::single(Atomic::TString))
1725 } else {
1726 None
1727 }
1728 });
1729 assert!(
1730 result
1731 .types
1732 .iter()
1733 .all(|t| !matches!(t, Atomic::TConditional { .. })),
1734 "no TConditional should survive: {:?}",
1735 result.types
1736 );
1737 assert_eq!(result.types.len(), 1);
1738 assert!(matches!(result.types[0], Atomic::TString));
1739 }
1740
1741 #[test]
1742 fn substitute_intersection_parts() {
1743 let ty = Type::single(Atomic::TIntersection {
1744 parts: vec_to_type_params(vec![
1745 Type::single(Atomic::TNamedObject {
1746 fqcn: Name::new("Countable"),
1747 type_params: empty_type_params(),
1748 }),
1749 t_param("T"),
1750 ]),
1751 });
1752 let result = ty.substitute_templates(&bindings_t_string());
1753 let Atomic::TIntersection { parts } = &result.types[0] else {
1754 panic!("expected TIntersection");
1755 };
1756 assert_eq!(parts.len(), 2);
1757 assert!(matches!(parts[0].types[0], Atomic::TNamedObject { .. }));
1758 assert!(matches!(parts[1].types[0], Atomic::TString));
1759 }
1760
1761 #[test]
1762 fn substitute_no_template_params_identity() {
1763 let ty = Type::single(Atomic::TInt);
1764 let result = ty.substitute_templates(&bindings_t_string());
1765 assert!(matches!(result.types[0], Atomic::TInt));
1766 }
1767}