1use super::{DiscriminantInfo, NarrowingContext, union_or_single_preserve};
13use crate::operations::property::{PropertyAccessEvaluator, PropertyAccessResult};
14use crate::relations::subtype::is_subtype_of;
15use crate::type_queries::{
16 LiteralValueKind, UnionMembersKind, classify_for_literal_value, classify_for_union_members,
17};
18use crate::types::{PropertyLookup, TypeId};
19use crate::visitor::{
20 intersection_list_id, is_literal_type_db, object_shape_id, object_with_index_shape_id,
21 union_list_id,
22};
23use rustc_hash::FxHashSet;
24use tracing::{Level, span, trace};
25use tsz_common::interner::Atom;
26
27impl<'a> NarrowingContext<'a> {
28 pub fn find_discriminants(&self, union_type: TypeId) -> Vec<DiscriminantInfo> {
34 let _span = span!(
35 Level::TRACE,
36 "find_discriminants",
37 union_type = union_type.0
38 )
39 .entered();
40
41 let members = match union_list_id(self.db, union_type) {
42 Some(members_id) => self.db.type_list(members_id),
43 None => return vec![],
44 };
45
46 if members.len() < 2 {
47 trace!("Union has fewer than 2 members, skipping discriminant search");
48 return vec![];
49 }
50
51 let mut all_properties: Vec<Atom> = Vec::new();
53 let mut member_props: Vec<Vec<(Atom, TypeId)>> = Vec::new();
54
55 for &member in members.iter() {
56 if let Some(shape_id) = object_shape_id(self.db, member) {
57 let shape = self.db.object_shape(shape_id);
58 let props_vec: Vec<(Atom, TypeId)> = shape
59 .properties
60 .iter()
61 .map(|p| (p.name, p.type_id))
62 .collect();
63
64 for (name, _) in &props_vec {
66 if !all_properties.contains(name) {
67 all_properties.push(*name);
68 }
69 }
70 member_props.push(props_vec);
71 } else {
72 return vec![];
74 }
75 }
76
77 let mut discriminants = Vec::new();
79
80 for prop_name in &all_properties {
81 let mut is_discriminant = true;
82 let mut variants: Vec<(TypeId, TypeId)> = Vec::new();
83 let mut seen_literals: Vec<TypeId> = Vec::new();
84
85 for (i, props) in member_props.iter().enumerate() {
86 let prop_type = props
88 .iter()
89 .find(|(name, _)| name == prop_name)
90 .map(|(_, ty)| *ty);
91
92 match prop_type {
93 Some(ty) => {
94 if is_literal_type_db(self.db, ty) {
96 if seen_literals.contains(&ty) {
98 is_discriminant = false;
99 break;
100 }
101 seen_literals.push(ty);
102 variants.push((ty, members[i]));
103 } else {
104 is_discriminant = false;
105 break;
106 }
107 }
108 None => {
109 is_discriminant = false;
111 break;
112 }
113 }
114 }
115
116 if is_discriminant && !variants.is_empty() {
117 discriminants.push(DiscriminantInfo {
118 property_name: *prop_name,
119 variants,
120 });
121 }
122 }
123
124 discriminants
125 }
126
127 fn get_type_at_path(
141 &self,
142 mut type_id: TypeId,
143 path: &[Atom],
144 evaluator: &PropertyAccessEvaluator<'_>,
145 ) -> Option<TypeId> {
146 for (i, &prop_name) in path.iter().enumerate() {
147 if type_id == TypeId::ANY {
149 return Some(TypeId::ANY);
150 }
151
152 type_id = self.resolve_type(type_id);
154
155 if let Some(members_id) = union_list_id(self.db, type_id) {
157 let members = self.db.type_list(members_id);
158 let remaining_path = &path[i..];
159 let prop_types: Vec<TypeId> = members
160 .iter()
161 .filter_map(|&member| self.get_type_at_path(member, remaining_path, evaluator))
162 .collect();
163
164 if prop_types.is_empty() {
165 return None;
166 } else if prop_types.len() == 1 {
167 return Some(prop_types[0]);
168 }
169 return Some(self.db.union(prop_types));
170 }
171
172 let prop_name_arc = self.db.resolve_atom_ref(prop_name);
175 let prop_name_str = prop_name_arc.as_ref();
176 match evaluator.resolve_property_access(type_id, prop_name_str) {
177 PropertyAccessResult::Success {
178 type_id: prop_type_id,
179 ..
180 } => {
181 type_id = prop_type_id;
184 }
185 PropertyAccessResult::PropertyNotFound { .. } => {
186 return None;
189 }
190 PropertyAccessResult::PossiblyNullOrUndefined { property_type, .. } => {
191 if let Some(prop_ty) = property_type {
195 type_id = self.db.union2(prop_ty, TypeId::UNDEFINED);
197 } else {
198 type_id = TypeId::UNDEFINED;
200 }
201 }
202 PropertyAccessResult::IsUnknown => {
203 return Some(TypeId::ANY);
204 }
205 }
206 }
207
208 Some(type_id)
209 }
210
211 fn get_top_level_property_type_fast(&self, type_id: TypeId, property: Atom) -> Option<TypeId> {
217 let key = (type_id, property);
218 if let Some(&cached) = self.cache.property_cache.borrow().get(&key) {
219 return cached;
220 }
221
222 let result = self
224 .get_top_level_property_type_fast_uncached(type_id, property)
225 .map(|prop_type| self.resolve_type(prop_type));
226 self.cache.property_cache.borrow_mut().insert(key, result);
227 result
228 }
229
230 fn get_top_level_property_type_fast_uncached(
231 &self,
232 mut type_id: TypeId,
233 property: Atom,
234 ) -> Option<TypeId> {
235 type_id = self.resolve_type(type_id);
236
237 if intersection_list_id(self.db, type_id).is_some() {
240 return None;
241 }
242
243 let shape_id = object_shape_id(self.db, type_id)
244 .or_else(|| object_with_index_shape_id(self.db, type_id))?;
245 let shape = self.db.object_shape(shape_id);
246
247 let prop = match self.db.object_property_index(shape_id, property) {
248 PropertyLookup::Found(idx) => shape.properties.get(idx),
249 PropertyLookup::NotFound => None,
250 PropertyLookup::Uncached => {
251 shape
253 .properties
254 .binary_search_by_key(&property, |p| p.name)
255 .ok()
256 .and_then(|idx| shape.properties.get(idx))
257 }
258 }?;
259
260 Some(if prop.optional {
261 self.db.union2(prop.type_id, TypeId::UNDEFINED)
262 } else {
263 prop.type_id
264 })
265 }
266
267 #[inline]
272 fn literal_subtype_fast(&self, source: TypeId, target: TypeId) -> Option<bool> {
273 if source == target {
274 return Some(true);
275 }
276
277 match (
278 classify_for_literal_value(self.db, source),
279 classify_for_literal_value(self.db, target),
280 ) {
281 (LiteralValueKind::String(a), LiteralValueKind::String(b)) => Some(a == b),
282 (LiteralValueKind::Number(a), LiteralValueKind::Number(b)) => Some(a == b),
283 (LiteralValueKind::String(_), LiteralValueKind::Number(_))
284 | (LiteralValueKind::Number(_), LiteralValueKind::String(_)) => Some(false),
285 _ => None,
286 }
287 }
288
289 fn fast_narrow_top_level_discriminant(
294 &self,
295 original_union_type: TypeId,
296 members: &[TypeId],
297 property: Atom,
298 literal_value: TypeId,
299 keep_matching: bool,
300 ) -> Option<TypeId> {
301 let mut kept = Vec::with_capacity(members.len());
302
303 for &member in members {
304 if member.is_any_or_unknown() {
305 kept.push(member);
306 continue;
307 }
308
309 let prop_type = self.get_top_level_property_type_fast(member, property)?;
310 let should_keep = if prop_type == literal_value {
311 keep_matching
312 } else if keep_matching {
313 self.literal_subtype_fast(literal_value, prop_type)
315 .unwrap_or_else(|| is_subtype_of(self.db, literal_value, prop_type))
316 } else {
317 !self
319 .literal_subtype_fast(prop_type, literal_value)
320 .unwrap_or_else(|| is_subtype_of(self.db, prop_type, literal_value))
321 };
322
323 if should_keep {
324 kept.push(member);
325 }
326 }
327
328 if keep_matching && kept.is_empty() {
329 return Some(TypeId::NEVER);
330 }
331 if keep_matching && kept.len() == members.len() {
332 return Some(original_union_type);
333 }
334
335 Some(union_or_single_preserve(self.db, kept))
336 }
337
338 pub fn narrow_by_discriminant_for_type(
353 &self,
354 type_id: TypeId,
355 prop_path: &[Atom],
356 literal_type: TypeId,
357 is_true_branch: bool,
358 ) -> TypeId {
359 use crate::type_queries::{
360 TypeParameterConstraintKind, classify_for_type_parameter_constraint,
361 };
362
363 if let TypeParameterConstraintKind::TypeParameter {
364 constraint: Some(constraint),
365 } = classify_for_type_parameter_constraint(self.db, type_id)
366 && constraint != type_id
367 {
368 let narrowed_constraint = if is_true_branch {
369 self.narrow_by_discriminant(constraint, prop_path, literal_type)
370 } else {
371 self.narrow_by_excluding_discriminant(constraint, prop_path, literal_type)
372 };
373 if narrowed_constraint != constraint {
374 return self.db.intersection(vec![type_id, narrowed_constraint]);
375 }
376 }
377
378 if is_true_branch {
379 self.narrow_by_discriminant(type_id, prop_path, literal_type)
380 } else {
381 self.narrow_by_excluding_discriminant(type_id, prop_path, literal_type)
382 }
383 }
384
385 pub fn narrow_by_property_truthiness(
389 &self,
390 union_type: TypeId,
391 property_path: &[Atom],
392 sense: bool,
393 ) -> TypeId {
394 let _span = span!(
395 Level::TRACE,
396 "narrow_by_property_truthiness",
397 union_type = union_type.0,
398 property_path_len = property_path.len(),
399 sense
400 )
401 .entered();
402
403 let resolved_type = self.resolve_type(union_type);
404
405 let single_member_storage: Vec<TypeId>;
406 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
407 UnionMembersKind::Union(members_list) => {
408 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
409 &single_member_storage
410 }
411 UnionMembersKind::NotUnion => {
412 single_member_storage = vec![resolved_type];
413 &single_member_storage
414 }
415 };
416
417 let mut matching: Vec<TypeId> = Vec::new();
418 let property_evaluator = match self.resolver {
419 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
420 None => PropertyAccessEvaluator::new(self.db),
421 };
422
423 for &member in members {
424 if member.is_any_or_unknown() {
425 matching.push(member);
426 continue;
427 }
428
429 let resolved_member = self.resolve_type(member);
430
431 let intersection_members = intersection_list_id(self.db, resolved_member)
432 .map(|members_id| self.db.type_list(members_id).to_vec());
433
434 let check_member_for_property = |check_type_id: TypeId| -> bool {
435 let prop_type = match self.get_type_at_path(
436 check_type_id,
437 property_path,
438 &property_evaluator,
439 ) {
440 Some(t) => t,
441 None => {
442 return !sense;
444 }
445 };
446
447 let resolved_prop_type = self.resolve_type(prop_type);
448
449 if sense {
452 let narrowed = self.narrow_by_truthiness(resolved_prop_type);
453 narrowed != TypeId::NEVER
454 } else {
455 let narrowed = self.narrow_to_falsy(resolved_prop_type);
456 narrowed != TypeId::NEVER
457 }
458 };
459
460 let matches = if let Some(ref intersection) = intersection_members {
461 intersection.iter().any(|&m| check_member_for_property(m))
462 } else {
463 check_member_for_property(resolved_member)
464 };
465
466 if matches {
467 matching.push(member);
468 }
469 }
470
471 if matching.is_empty() {
472 return TypeId::NEVER;
473 }
474
475 if matching.len() == members.len() {
476 return union_type;
477 }
478
479 union_or_single_preserve(self.db, matching)
480 }
481
482 pub fn narrow_by_discriminant(
486 &self,
487 union_type: TypeId,
488 property_path: &[Atom],
489 literal_value: TypeId,
490 ) -> TypeId {
491 let _span = span!(
492 Level::TRACE,
493 "narrow_by_discriminant",
494 union_type = union_type.0,
495 property_path_len = property_path.len(),
496 literal_value = literal_value.0
497 )
498 .entered();
499
500 let resolved_type = self.resolve_type(union_type);
503
504 trace!(
505 "narrow_by_discriminant: union_type={}, resolved_type={}, property_path={:?}, literal_value={}",
506 union_type.0, resolved_type.0, property_path, literal_value.0
507 );
508
509 let single_member_storage: Vec<TypeId>;
512 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
513 UnionMembersKind::Union(members_list) => {
514 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
516 &single_member_storage
517 }
518 UnionMembersKind::NotUnion => {
519 single_member_storage = vec![resolved_type];
521 &single_member_storage
522 }
523 };
524
525 trace!("narrow_by_discriminant: members={:?}", members);
526
527 trace!(
528 "Checking {} member(s) for discriminant match",
529 members.len()
530 );
531
532 trace!(
533 "Narrowing union with {} members by discriminant property",
534 members.len()
535 );
536
537 if property_path.len() == 1
538 && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
539 union_type,
540 members,
541 property_path[0],
542 literal_value,
543 true,
544 )
545 {
546 return fast_result;
547 }
548
549 let mut matching: Vec<TypeId> = Vec::new();
550 let property_evaluator = match self.resolver {
551 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
552 None => PropertyAccessEvaluator::new(self.db),
553 };
554
555 for &member in members {
556 if member.is_any_or_unknown() {
558 trace!("Member {} is any/unknown, keeping in true branch", member.0);
559 matching.push(member);
560 continue;
561 }
562
563 let resolved_member = self.resolve_type(member);
566
567 let intersection_members = intersection_list_id(self.db, resolved_member)
569 .map(|members_id| self.db.type_list(members_id).to_vec());
570
571 let check_member_for_property = |check_type_id: TypeId| -> bool {
573 let prop_type = match self.get_type_at_path(
575 check_type_id,
576 property_path,
577 &property_evaluator,
578 ) {
579 Some(t) => t,
580 None => {
581 trace!(
583 "Member {} does not have property path {:?}",
584 check_type_id.0, property_path
585 );
586 return false;
587 }
588 };
589
590 let resolved_prop_type = self.resolve_type(prop_type);
594
595 let matches = is_subtype_of(self.db, literal_value, resolved_prop_type);
598
599 if matches {
600 trace!(
601 "Member {} has property path {:?} with type {}, literal {} matches",
602 check_type_id.0, property_path, prop_type.0, literal_value.0
603 );
604 } else {
605 trace!(
606 "Member {} has property path {:?} with type {}, literal {} does not match",
607 check_type_id.0, property_path, prop_type.0, literal_value.0
608 );
609 }
610
611 matches
612 };
613
614 let has_property_match = if let Some(ref intersection) = intersection_members {
616 intersection.iter().any(|&m| check_member_for_property(m))
618 } else {
619 check_member_for_property(resolved_member)
621 };
622
623 if has_property_match {
624 matching.push(member);
625 }
626 }
627
628 if matching.is_empty() {
631 trace!("No members matched discriminant check, returning never");
632 TypeId::NEVER
633 } else if matching.len() == members.len() {
634 trace!("All members matched, returning original");
635 union_type
636 } else if matching.len() == 1 {
637 trace!("Narrowed to single member");
638 matching[0]
639 } else {
640 trace!(
641 "Narrowed to {} of {} members",
642 matching.len(),
643 members.len()
644 );
645 self.db.union(matching)
646 }
647 }
648
649 pub fn narrow_by_excluding_discriminant(
666 &self,
667 union_type: TypeId,
668 property_path: &[Atom],
669 excluded_value: TypeId,
670 ) -> TypeId {
671 let _span = span!(
672 Level::TRACE,
673 "narrow_by_excluding_discriminant",
674 union_type = union_type.0,
675 property_path_len = property_path.len(),
676 excluded_value = excluded_value.0
677 )
678 .entered();
679
680 let resolved_type = self.resolve_type(union_type);
683
684 let single_member_storage: Vec<TypeId>;
688 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
689 UnionMembersKind::Union(members_list) => {
690 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
691 &single_member_storage
692 }
693 UnionMembersKind::NotUnion => {
694 single_member_storage = vec![resolved_type];
695 &single_member_storage
696 }
697 };
698
699 trace!(
700 "Excluding discriminant value {} from union with {} members",
701 excluded_value.0,
702 members.len()
703 );
704
705 if property_path.len() == 1
706 && let Some(fast_result) = self.fast_narrow_top_level_discriminant(
707 union_type,
708 members,
709 property_path[0],
710 excluded_value,
711 false,
712 )
713 {
714 return fast_result;
715 }
716
717 let mut remaining: Vec<TypeId> = Vec::new();
718 let property_evaluator = match self.resolver {
719 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
720 None => PropertyAccessEvaluator::new(self.db),
721 };
722
723 for &member in members {
724 if member.is_any_or_unknown() {
726 trace!(
727 "Member {} is any/unknown, keeping in false branch",
728 member.0
729 );
730 remaining.push(member);
731 continue;
732 }
733
734 let resolved_member = self.resolve_type(member);
736
737 let intersection_members = intersection_list_id(self.db, resolved_member)
739 .map(|members_id| self.db.type_list(members_id).to_vec());
740
741 let should_keep_member = |check_type_id: TypeId| -> bool {
744 let prop_type = match self.get_type_at_path(
746 check_type_id,
747 property_path,
748 &property_evaluator,
749 ) {
750 Some(t) => t,
751 None => {
752 trace!(
754 "Member {} does not have property path, keeping",
755 check_type_id.0
756 );
757 return true;
758 }
759 };
760
761 let resolved_prop_type = self.resolve_type(prop_type);
763
764 let should_exclude = is_subtype_of(self.db, resolved_prop_type, excluded_value);
768
769 if should_exclude {
770 trace!(
771 "Member {} has property path type {} which is subtype of excluded {}, excluding",
772 check_type_id.0, prop_type.0, excluded_value.0
773 );
774 false } else {
776 trace!(
777 "Member {} has property path type {} which is not subtype of excluded {}, keeping",
778 check_type_id.0, prop_type.0, excluded_value.0
779 );
780 true }
782 };
783
784 let keep_member = if let Some(ref intersection) = intersection_members {
786 intersection.iter().all(|&m| should_keep_member(m))
792 } else {
793 should_keep_member(resolved_member)
795 };
796
797 if keep_member {
798 remaining.push(member);
799 }
800 }
801
802 union_or_single_preserve(self.db, remaining)
803 }
804
805 pub fn narrow_by_excluding_discriminant_values(
809 &self,
810 union_type: TypeId,
811 property_path: &[Atom],
812 excluded_values: &[TypeId],
813 ) -> TypeId {
814 if excluded_values.is_empty() {
815 return union_type;
816 }
817
818 let _span = span!(
819 Level::TRACE,
820 "narrow_by_excluding_discriminant_values",
821 union_type = union_type.0,
822 property_path_len = property_path.len(),
823 excluded_count = excluded_values.len()
824 )
825 .entered();
826
827 let resolved_type = self.resolve_type(union_type);
828
829 let single_member_storage: Vec<TypeId>;
830 let members: &[TypeId] = match classify_for_union_members(self.db, resolved_type) {
831 UnionMembersKind::Union(members_list) => {
832 single_member_storage = members_list.into_iter().collect::<Vec<_>>();
833 &single_member_storage
834 }
835 UnionMembersKind::NotUnion => {
836 single_member_storage = vec![resolved_type];
837 &single_member_storage
838 }
839 };
840
841 let excluded_set: FxHashSet<TypeId> = excluded_values.iter().copied().collect();
843
844 let mut remaining: Vec<TypeId> = Vec::new();
845 let property_evaluator = match self.resolver {
846 Some(resolver) => PropertyAccessEvaluator::with_resolver(self.db, resolver),
847 None => PropertyAccessEvaluator::new(self.db),
848 };
849
850 for &member in members {
851 if member.is_any_or_unknown() {
852 remaining.push(member);
853 continue;
854 }
855
856 let resolved_member = self.resolve_type(member);
857 let intersection_members = intersection_list_id(self.db, resolved_member)
858 .map(|members_id| self.db.type_list(members_id).to_vec());
859
860 let should_keep_member = |check_type_id: TypeId| -> bool {
862 let prop_type = match self.get_type_at_path(
863 check_type_id,
864 property_path,
865 &property_evaluator,
866 ) {
867 Some(t) => t,
868 None => return true, };
870
871 let resolved_prop_type = self.resolve_type(prop_type);
872
873 if excluded_set.contains(&resolved_prop_type) {
875 return false; }
877
878 for &excluded in excluded_values {
880 if is_subtype_of(self.db, resolved_prop_type, excluded) {
881 return false; }
883 }
884 true };
886
887 let keep_member = if let Some(ref intersection) = intersection_members {
888 intersection.iter().all(|&m| should_keep_member(m))
889 } else {
890 should_keep_member(resolved_member)
891 };
892
893 if keep_member {
894 remaining.push(member);
895 }
896 }
897
898 union_or_single_preserve(self.db, remaining)
899 }
900}